更新:これを行うより良い方法があります。コメントを参照してください。
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
できるため、なりすましを検出して自分で確認できます。