17

中間者攻撃 (他の誰かになりすましたサーバー) を防ぐために、SSL 経由で接続している SMTP サーバーにも有効な SSL 証明書があることを確認したいと思います。

たとえば、ポート 25 で SMTP サーバーに接続した後、次のように安全な接続に切り替えることができます。

<?php

$smtp = fsockopen( "tcp://mail.example.com", 25, $errno, $errstr ); 
fread( $smtp, 512 ); 

fwrite($smtp,"HELO mail.example.me\r\n"); // .me is client, .com is server
fread($smtp, 512); 

fwrite($smtp,"STARTTLS\r\n");
fread($smtp, 512); 

stream_socket_enable_crypto( $smtp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT ); 

fwrite($smtp,"HELO mail.example.me\r\n");

ただし、PHP が SSL 証明書をチェックしている場所については言及されていません。PHP にはルート CA のリストが組み込まれていますか? 何でも受け入れるだけですか?

証明書が有効であり、SMTP サーバーが実際に私が思っているとおりのものであることを確認する適切な方法は何ですか?

アップデート

PHP.netに関するこのコメントに基づいて、いくつかのストリーム オプションを使用して SSL チェックを実行できるようです。最良の部分は、stream_context_set_optionがコンテキストまたはストリーム リソースを受け入れることです。したがって、TCP 接続のある時点で、CA 証明書バンドルを使用して SSL に切り替えることができます。

$resource = fsockopen( "tcp://mail.example.com", 25, $errno, $errstr ); 

...

stream_set_blocking($resource, true);

stream_context_set_option($resource, 'ssl', 'verify_host', true);
stream_context_set_option($resource, 'ssl', 'verify_peer', true);
stream_context_set_option($resource, 'ssl', 'allow_self_signed', false);

stream_context_set_option($resource, 'ssl', 'cafile', __DIR__ . '/cacert.pem');

$secure = stream_socket_enable_crypto($resource, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
stream_set_blocking($resource, false);

if( ! $secure)
{
    die("failed to connect securely\n");
}

また、SSL オプションを拡張するコンテキスト オプションとパラメータも参照してください。

ただし、これで主な問題は解決しましたが、有効な証明書が実際に接続先のドメイン/IP に属していることを確認するにはどうすればよいですか?

つまり、私が接続しているサーバーの証明書にも有効な証明書がある可能性がありますが、有効な証明書を使用して「example.com」のように動作する別のサーバーではなく、「example.com」に対して有効であることをどのように確認できますか?

更新 2

steam コンテキスト パラメータを使用して SSL 証明書をキャプチャし、 openssl_x509_parseで解析できるようです。

$cont = stream_context_get_params($r);
print_r(openssl_x509_parse($cont["options"]["ssl"]["peer_certificate"]));
4

3 に答える 3

8

更新:これを行うより良い方法があります。コメントを参照してください。

openssl証明書を取得し、フィルターとして使用してサーバーと会話することができます。このようにして、同じ接続中に証明書を抽出して調べることができます。

これは不完全な実装です (実際のメール送信会話は存在しません)、開始する必要があります:

<?php
    $server = 'smtp.gmail.com';

    $pid    = proc_open("openssl s_client -connect $server:25 -starttls smtp",
                    array(
                            0 => array('pipe', 'r'),
                            1 => array('pipe', 'w'),
                            2 => array('pipe', 'r'),
                    ),
                    $pipes,
                    '/tmp',
                    array()
            );
    list($smtpout, $smtpin, $smtperr) = $pipes; unset($pipes);

    $stage  = 0;
    $cert   = 0;
    $certificate = '';
    while(($stage < 5) && (!feof($smtpin)))
    {
            $line = fgets($smtpin, 1024);
            switch(trim($line))
            {
                    case '-----BEGIN CERTIFICATE-----':
                            $cert   = 1;
                            break;
                    case '-----END CERTIFICATE-----':
                            $certificate .= $line;
                            $cert   = 0;
                            break;
                    case '---':
                            $stage++;
            }
            if ($cert)
                    $certificate .= $line;
    }
    fwrite($smtpout,"HELO mail.example.me\r\n"); // .me is client, .com is server
    print fgets($smtpin, 512);
    fwrite($smtpout,"QUIT\r\n");
    print fgets($smtpin, 512);

    fclose($smtpin);
    fclose($smtpout);
    fclose($smtperr);
    proc_close($pid);

    print $certificate;

    $par    = openssl_x509_parse($certificate);
?>

もちろん、意味のあるものをサーバーに送信する前に、証明書の解析と確認を行います。

配列には、$parサブジェクトと同じように解析された名前が (残りの中で) 見つかるはずです。

Array
(
    [name] => /C=US/ST=California/L=Mountain View/O=Google Inc/CN=smtp.gmail.com
    [subject] => Array
        (
            [C] => US
            [ST] => California
            [L] => Mountain View
            [O] => Google Inc
            [CN] => smtp.gmail.com
        )

    [hash] => 11e1af25
    [issuer] => Array
        (
            [C] => US
            [O] => Google Inc
            [CN] => Google Internet Authority
        )

    [version] => 2
    [serialNumber] => 280777854109761182656680
    [validFrom] => 120912115750Z
    [validTo] => 130607194327Z
    [validFrom_time_t] => 1347451070
    [validTo_time_t] => 1370634207
    ...
    [extensions] => Array
        (
            ...
            [subjectAltName] => DNS:smtp.gmail.com
        )

SSL が独自に行う日付チェックなどとは別に、有効性をチェックするには、次のいずれかの条件が適用されることを確認する必要があります。

  • エンティティの CN は DNS 名です。たとえば、「CN = smtp.your.server.com」です。

  • 定義された拡張機能があり、それらには subjectAltName が含まれておりexplode(',', $subjectAltName)一度. 一致するものがない場合、証明書は拒否されます。DNS:

PHP での証明書の検証

さまざまなソフトウェアでの検証ホストの意味は、せいぜいあいまいに思えます。

そこで、この問題の真相を探ることに決め、OpenSSL のソース コード (openssl-1.0.1c) をダウンロードして、自分で調べてみました。

私が期待していたコードへの参照は見つかりませんでした。

  • コロンで区切られた文字列の解析を試みます
  • への参照subjectAltName(OpenSSL が呼び出すSN_subject_alt_name)
  • 区切り文字として「DNS[:]」を使用

OpenSSL はすべての証明書の詳細を構造に入れ、それらのいくつかに対して非常に基本的なテストを実行するようですが、ほとんどの「人間が読める」フィールドはそのままにしておきます。それは理にかなっています: 名前チェックは証明書署名チェックよりも高いレベルにあると主張することができます

次に、最新の cURL と最新の PHP tarball もダウンロードしました。

PHP ソース コードにも何も見つかりませんでした。明らかに、オプションはそのまま渡され、それ以外の場合は無視されます。このコードは警告なしで実行されました:

    stream_context_set_option($smtp, 'ssl', 'I-want-a-banana', True);

そしてstream_context_get_options後で忠実に回収された

    [ssl] => Array
        (
            [I-want-a-banana] => 1
            ...

これも理にかなっています。PHP は、「context-option-setting」コンテキストでは、どのオプションが使用されるかを知ることができません。

同様に、証明書解析コード証明書を解析し、そこに OpenSSL が配置した情報を抽出しますが、同じ情報を検証しません。

そこで、もう少し深く掘り下げて、最終的に cURL で証明書検証コードを見つけました。

// curl-7.28.0/lib/ssluse.c

static CURLcode verifyhost(struct connectdata *conn,
                       X509 *server_cert)
{

ここで、私が期待したことを行います: subjectAltNames を探し、それらすべての正気をチェックしhostmatch、hello.example.com == *.example.com などのチェックが実行される場所でそれらを実行します。追加のサニティ チェックがあります。および xn_checks。

要約すると、OpenSSL はいくつかの簡単なチェックを実行し、残りは呼び出し元に任せます。OpenSSL を呼び出す cURL は、より多くのチェックを実装します。PHP もCN でいくつかのverify_peerチェックを実行しますが、そのままにしておきsubjectAltNameます。これらのチェックは、私をあまり納得させません。以下の「テスト」を参照してください。

cURL の関数にアクセスする機能がないため、最良の代替手段はPHP で再実装することです。

たとえば、可変ワイルドカード ドメイン マッチングは、実際のドメインと証明書ドメインの両方をドット展開し、2 つの配列を逆にすることで実行できます。

com.example.site.my
com.example.*

対応する項目が等しいか、証明書が * であることを確認します。その場合は、ここcomとの少なくとも 2 つのコンポーネントを既にチェックしている必要がありexampleます。

証明書を一度に確認したい場合は、上記のソリューションが最適なソリューションの 1 つだと思います。opensslクライアントに頼らずにストリームを直接開くことができればさらに良いでしょう。これは可能です。コメントを参照してください。

テスト

Thawte から "mail.eve.com" 宛てに発行された、有効で完全に信頼できる有効な証明書があります。

Alice で実行されている上記のコードはmail.eve.com、 と安全に接続し、期待どおりに接続します。

次に、同じ証明書を にインストールしますmail.bob.com。または、別の方法で、サーバーが Bob であることを DNS に納得させますが、実際にはまだ Eve です。

SSL 接続は引き続き機能すると思います (証明書有効で信頼されています) が、証明書は Bob に対して発行されたものではなく、Eve に対して発行されたものです。したがって、誰かがこの最後のチェックを行い、Bob が実際に Eve によって偽装されていること (または同等に、Bob が Eve の盗まれた証明書を使用していること) を Alice に警告する必要があります。

以下のコードを使用しました。

    $smtp = fsockopen( "tcp://mail.bob.com", 25, $errno, $errstr );
    fread( $smtp, 512 );
    fwrite($smtp,"HELO alice\r\n");
    fread($smtp, 512);
    fwrite($smtp,"STARTTLS\r\n");
    fread($smtp, 512);
    stream_set_blocking($smtp, true);
    stream_context_set_option($smtp, 'ssl', 'verify_host', true);
    stream_context_set_option($smtp, 'ssl', 'verify_peer', true);
    stream_context_set_option($smtp, 'ssl', 'allow_self_signed', false);
    stream_context_set_option($smtp, 'ssl', 'cafile', '/etc/ssl/cacert.pem');
    $secure = stream_socket_enable_crypto($smtp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
    stream_set_blocking($smtp, false);
    print_r(stream_context_get_options($smtp));
    if( ! $secure)
            die("failed to connect securely\n");
    print "Success!\n";

と:

  • 証明書が信頼できる機関で 検証できない場合:
    • verify_host は何もしません
    • verify_peer TRUE はエラーを引き起こします
    • verify_peer FALSE は接続を許可します
    • allow_self_signed は何もしません
  • 証明書の有効期限が切れている場合:
    • エラーが発生します。
  • 証明書が検証可能な場合:
    • 「mail.bob.com」になりすまして「mail.eve.com」への接続が許可され、「Success!」というメッセージが表示されます。メッセージ。

これは、私の愚かなエラーがなければ、PHP 自体は証明書を names に対してチェックしないことを意味します。

この投稿の冒頭にあるコードを使用するproc_openと、再び接続できますが、今回は にアクセスsubjectAltNameできるため、なりすましを検出して自分で確認できます。

于 2012-11-18T01:10:40.500 に答える
2

有効な証明書が実際に接続しているドメイン/IP に属していることを確認するにはどうすればよいですか?

証明書はドメイン名に対して発行されます (IP に対して発行されることはありません)。単一のドメイン名 ( などmail.example.com) またはワイルドカード*.example.com) にすることができます。openssl で証明書をデコードしたら、common namefield から呼び出されるその名前を読み取ることができますcn。次に、接続しようとしているマシンが証明書のマシンであるかどうかを確認する必要があります。すでに接続しているのでリモートピア名を取得しているため、チェックは非常に簡単ですが、どのように偏執的なチェックを実行したいかによっては、mail.example.comホスト名を解決する汚染された DNS を使用していないかどうかを確認しようとする場合があります。偽造IPへ。これは、最初に gethostbynamel() で解決することによって行う必要がmail.example.comありますこれにより、少なくとも 1 つの IP アドレスが得られます (1.2.3.4 だけを取得したとしましょう)。次に、返された IP アドレスごとにgethostbyaddr()で逆引き DNS をチェックすると、そのうちの 1 つが返されるはずです (サーバーが名前ごとに複数の IP アドレスを割り当てられることはまれではないため、mail.example.comを使用したことに注意してください)。gethostbynamel()gethostbyname()

注:厳しすぎるポリシーの適用には注意してください。ユーザーを傷つける可能性があります。単一のサーバーが多くのドメインをホストすることは、非常に一般的なシナリオです (共有ホスティングのように)。このような場合、サーバーは IP を使用して1.2.3.4おり、顧客のドメインexample.comにはその IP アドレスが与えられています (したがって、解決するexample.comと が得られますが1.2.3.4、このホストの逆引き DNS は、やなどの顧客のドメインではなく、ISP ドメイン名に結合される可能性が高くなります。また、技術的には単一の IP を複数のドメイン名に同時に割り当てることができるため、ホスティング業者はこれを行いますが、リバース DNS を使用すると、1 つの IP を割り当てることができます。box0123.hosterdomain.com4-3-2-1.hosterdomain.comIP ごとのエントリのみ。また、顧客の代わりに独自のドメイン名を使用することで、顧客がサーバーに追加またはサーバーから削除されても revDNS を気にする必要はありません。

したがって、接続先のホストのリストが閉じている場合は、このテストを実行できますが、ユーザーがどこにでも接続しようとする可能性がある場合は、証明書チェーンのみをチェックすることに固執します.

編集#1

自分が制御していない DNS にクエリを実行すると、完全に信頼することはできません。そのような DNS はゾンビ化され、汚染され、「転送」( FQDNから IP へ) と逆方向 (IP から FQDN へ)の両方で、常に嘘をつき、彼に尋ねたクエリに対する偽の応答をすることができます。DNSサーバーがハッキングされた(ルート化された)場合、(攻撃者が十分に動機付けられている場合)in-addr.arpaクエリを転送せず、他の応答と一致するように応答を偽造することができます(逆引きの詳細はこちら). 実際、DNSSECを使用しない限り小切手をだます方法はまだあります。したがって、どのように行動する必要があるかを考える必要があります-フォワードクエリはDNSポイズニングによってスプーフィングされる可能性がありますが、ホストがあなたのものではない場合、これは逆引きでは機能しません(逆引きDNSゾーンは他のサーバーでホストされていることを意味します) 1 つは通常のクエリに応答します)。複数の DNS を直接クエリすることで、ローカル DNS ポイズニングから身を守ろうとすることができます。すべて問題なければ、すべての DNS クエリで同じ答えが得られるはずです。何かが怪しい場合は、いくつかの応答が異なりますが、これは簡単に検出できます。

したがって、それはすべて、どの程度安全になりたいか、何を達成したいかによって異なります。高度なセキュリティが必要な場合は、「パブリック」サービスを使用せず、VPN を使用してトラフィックをターゲット サーバーに直接トンネリングする必要があります。

編集#2

IPv4 と IPv6 については、PHP には両方の機能がないため、上記のチェックを行いたい場合hostは、仕事をする (または PHP 拡張機能を作成する) ようなツールを呼び出すことを検討したいと思います。

于 2012-11-18T00:23:29.723 に答える