C#でSSL(サーバー証明書の検証あり版)

C#SSL通信を行う。

サーバー証明書の検証も行う。



例として、インターネット上のWebサーバーとSSL通信を行う。


手順は、以下のようになる。

1Webサーバー名からWebサーバーのIPアドレスDNSでルックアップ
2TcpClientを用いて、Webサーバーの443番ポートに接続する
3SslStreamを生成し、でSSL通信を開始
4SslStream#AuthenticateAsClientメソッドで、サーバー証明書の検証を行う
5HTTPリクエストを送信する
6HTTPレスポンスを受信する


PrintCertificateメソッドは、サーバー証明書の内容をコンソールに表示するためのメソッド。
RemoteCertificateValidationCallbackメソッドで、証明書の検証を行なっている。

using System;
using System.Net;
using System.Net.Sockets;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.IO;
using System.Text;

public class Program
{
    //証明書の内容を表示するメソッド
    private static void PrintCertificate(X509Certificate certificate)
    {
        Console.WriteLine("===========================================");
        Console.WriteLine("Subject={0}", certificate.Subject);
        Console.WriteLine("Issuer={0}", certificate.Issuer);
        Console.WriteLine("Format={0}", certificate.GetFormat());
        Console.WriteLine("ExpirationDate={0}", certificate.GetExpirationDateString());
        Console.WriteLine("EffectiveDate={0}", certificate.GetEffectiveDateString());
        Console.WriteLine("KeyAlgorithm={0}", certificate.GetKeyAlgorithm());
        Console.WriteLine("PublicKey={0}", certificate.GetPublicKeyString());
        Console.WriteLine("SerialNumber={0}", certificate.GetSerialNumberString());
        Console.WriteLine("===========================================");
    }

    //サーバー証明書を検証するためのコールバックメソッド
    private static Boolean RemoteCertificateValidationCallback(Object sender, 
        X509Certificate certificate, 
        X509Chain chain, 
        SslPolicyErrors sslPolicyErrors)
    {
        PrintCertificate(certificate);

        if (sslPolicyErrors == SslPolicyErrors.None)
        {
            Console.WriteLine("サーバー証明書の検証に成功しました\n");
            return true;
        }
        else
        {
            //何かサーバー証明書検証エラーが発生している

            //SslPolicyErrors列挙体には、Flags属性があるので、
            //エラーの原因が複数含まれているかもしれない。
            //そのため、&演算子で1つ1つエラーの原因を検出する。
            if ((sslPolicyErrors & SslPolicyErrors.RemoteCertificateChainErrors) == 
                SslPolicyErrors.RemoteCertificateChainErrors)
            {
                Console.WriteLine("ChainStatusが、空でない配列を返しました");
            }

            if ((sslPolicyErrors & SslPolicyErrors.RemoteCertificateNameMismatch) == 
                SslPolicyErrors.RemoteCertificateNameMismatch)
            {
                Console.WriteLine("証明書名が不一致です");
            }

            if ((sslPolicyErrors & SslPolicyErrors.RemoteCertificateNotAvailable) == 
                SslPolicyErrors.RemoteCertificateNotAvailable)
            {
                Console.WriteLine("証明書が利用できません");
            }

            //検証失敗とする
            return false;
        }
    }

    public static void Main()
    {
        //ホスト名とポート番号を指定
        String hostName = "google.com";
        Int32 port = 443;

        using (TcpClient client = new TcpClient())
        {
            //接続先Webサーバー名からIPアドレスをルックアップ
            IPAddress[] ipAddresses = 
                Dns.GetHostAddresses(hostName);

            //Webサーバーに接続する
            client.Connect(new IPEndPoint(ipAddresses[0], port));
            
            //SSL通信の開始
            using (SslStream sslStream = 
                new SslStream(client.GetStream(), false, RemoteCertificateValidationCallback))
            {
                //サーバーの認証を行う
                //これにより、RemoteCertificateValidationCallbackメソッドが呼ばれる
                sslStream.AuthenticateAsClient(hostName);

                //HTTPリクエストをサーバーに送信する
                Byte[] req = 
                    Encoding.ASCII.GetBytes(String.Format("GET / HTTP/1.0\r\nHost: {0}\r\n\r\n", hostName));
                sslStream.Write(req);
                sslStream.Flush();

                //サーバーから受信したHTTPレスポンスを読み込んで
                //コンソールに表示数
                Byte[] res = new Byte[1024];
                Int32 n;
                while ( (n = sslStream.Read(res, 0, res.Length) ) > 0)
                {
                    String s = Encoding.ASCII.GetString(res, 0, n);
                    Console.WriteLine(s);
                }
            }
        }
    }
}





ソースコードの補足説明

SslStreamクラスコンストラクタの第3引数で指定した、RemoteCertificateValidationCallbackというコールバックメソッドでサーバー証明書の検証を行っている。


RemoteCertificateValidationCallbackは、デリゲートとして以下のように定義されている。

public delegate bool RemoteCertificateValidationCallback(
	Object sender,
	X509Certificate certificate,
	X509Chain chain,
	SslPolicyErrors sslPolicyErrors
)



サーバー証明書の検証を行うためには、このメソッドの第3引数である、sslPolicyErrorsを用いる。
sslPolicyErrorsは、SslPolicyErrors列挙体のオブジェクトであり、SslPolicyErrors列挙体は、以下のように定義されている。

namespace System.Net.Security
{
    // 概要:
    //     SSL (Secure Socket Layer) のポリシー エラーを列挙します。
    [Flags]
    public enum SslPolicyErrors
    {
        // 概要:
        //     SSL のポリシー エラーはありません。
        None = 0,
        //
        // 概要:
        //     証明書が利用できません。
        RemoteCertificateNotAvailable = 1,
        //
        // 概要:
        //     証明書名が不一致です。
        RemoteCertificateNameMismatch = 2,
        //
        // 概要:
        //     System.Security.Cryptography.X509Certificates.X509Chain.ChainStatus が、空でない配列を返しました。
        RemoteCertificateChainErrors = 4,
    }
}



SslPolicyErrors列挙体には Flags属性 が付いているため、サーバー証明書の検証エラーの複合的な原因を、&演算子を用いて詳細に調べることができる





実行結果

===========================================
Subject=CN=*.google.com, O=Google Inc, L=Mountain View, S=California, C=US
Issuer=CN=Google Internet Authority, O=Google Inc, C=US
Format=X509
ExpirationDate=2014/01/01 0:58:50
EffectiveDate=2013/04/11 21:52:18
KeyAlgorithm=1.2.840.10045.2.1
PublicKey=0481072B5CEC82040D3E50F8B7D07A47423C0CB215E0B716AD744E46166782A4729BCA
A1155C1332105EB0A331B721BB1917106E56464312861ED7ADD54464F4B1
SerialNumber=40950B0A00010000839D
===========================================
サーバー証明書の検証に成功しました

HTTP/1.0 301 Moved Permanently
Location: https://www.google.com/
Content-Type: text/html; charset=UTF-8
Date: Tue, 30 Apr 2013 11:28:01 GMT
Expires: Thu, 30 May 2013 11:28:01 GMT
Cache-Control: public, max-age=2592000
Server: gws
Content-Length: 220
X-XSS-Protection: 1; mode=block
X-Frame-Options: SAMEORIGIN

<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>301 Moved</TITLE></HEAD><BODY>
<H1>301 Moved</H1>
The document has moved
<A HREF="https://www.google.com/">here</A>.
</BODY></HTML>



実際には、Main関数のhostNameに不正なサーバー証明書を用いているWebサーバー名を指定しないと、検証エラーを発生させることができません。ですので自前でオレオレ証明書を作成して、自前のWebサーバーにセットして試すのが良いかと思います。