0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

TLS通信したい (3) Hello, world!

Last updated at Posted at 2025-08-31

1. はじめに

なんでも「とりあえず」ですが、ポート番号5000でやりとりすることを考えましょう。
サーバー側とクライアント側と二つのプログラムを作ることになります。

ChatGPTの出力が元になっています。利用される方は自己責任で。

2-1.サーバー側のコード

  1. サーバーは 自分で作ったサーバー証明書をロードし、クライアントを待ちます
  2. クライアントが接続し、クライアントが何か送信してきたら、それを返します
TlsServer.cs
using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography.X509Certificates;
using System.Net.Security;
using System.Text;

class TlsServer
{
    static void Main()
    {
        int port = 5000;
        string certPath = "server.pfx";
        string certPassword = "p@ssw0rd"; // PFX パスワード

        X509Certificate2 serverCertificate = new X509Certificate2(certPath, certPassword);

        TcpListener listener = new TcpListener(IPAddress.Any, port);
        listener.Start();
        Console.WriteLine($"Server listening on port {port}...");

        while (true)
        {
            TcpClient client = listener.AcceptTcpClient();
            Console.WriteLine("Client connected.");

            using (NetworkStream networkStream = client.GetStream())
            using (SslStream sslStream = new SslStream(networkStream, false))
            {
                sslStream.AuthenticateAsServer(serverCertificate, clientCertificateRequired: false, enabledSslProtocols: System.Security.Authentication.SslProtocols.Tls13, checkCertificateRevocation: false);
                Console.WriteLine("TLS handshake completed.");

                // クライアントから受信
                byte[] buffer = new byte[1024];
                int bytesRead = sslStream.Read(buffer, 0, buffer.Length);
                string message = Encoding.UTF8.GetString(buffer, 0, bytesRead);
                Console.WriteLine("Received from client: " + message);

                // そのまま返す
                byte[] response = Encoding.UTF8.GetBytes(message);
                sslStream.Write(response);
                sslStream.Flush();
            }
            client.Close();
        }
    }
}

2-2.(とりあえず)クライアント側のコード

  1. クライアントはサーバーに接続します
  2. サーバーを検証することは考えずに問答無用で認証します(受け入れます)
  3. サーバーに "Hello, world!" を送信します
  4. サーバーから送信された文字列を表示します
    この場合、送った "Hello, world!" が サーバーから送り返されてくるのでそれを表示します。
TlsClient.cs
using System;
using System.Net.Sockets;
using System.Net.Security;
using System.Security.Authentication;
using System.Text;

class TlsClientTest
{
    static void Main()
    {
        string server = "localhost";
        int port = 5000;

        TcpClient client = new TcpClient(server, port);
        // サーバ証明書を検証せずに、とりあえず承認する
        using (SslStream sslStream = new SslStream(client.GetStream(), false, (sender, cert, chain, errors) => true))
        {
            sslStream.AuthenticateAsClient(server, null, SslProtocols.Tls13, checkCertificateRevocation: false);
            Console.WriteLine("TLS handshake completed.");

            // サーバに送信
            string message = "Hello, world!";
            byte[] messageBytes = Encoding.UTF8.GetBytes(message);
            sslStream.Write(messageBytes);
            sslStream.Flush();

            // サーバから受信
            byte[] buffer = new byte[1024];
            int bytesRead = sslStream.Read(buffer, 0, buffer.Length);
            string response = Encoding.UTF8.GetString(buffer, 0, bytesRead);
            Console.WriteLine("Received from server: " + response);
        }

        client.Close();
    }
}

2-2.サーバー認証付のクライアントコード

真面目にサーバー認証をしてみましょう。
具体的には「サーバー証明書にはこう書かれているはず」を検証します。
サーバー証明書には 「CN=なんちゃらかんちゃら」とか、SANの部分にドメイン名が書いてあるはずです。

そこをチェックします。ChatGPTが吐き出したチェック部分のコードは難しい印象を受けます。むぅ。

TlsClient.cs
using System;
using System.Net.Security;
using System.Net.Sockets;
using System.Runtime.ConstrainedExecution;
using System.Security.Authentication;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;

class TlsClient
{
    static void Main()
    {
        string server = "localhost"; // 接続先サーバ名
        int port = 5000;

        TcpClient client = new TcpClient(server, port);

        using (SslStream sslStream = new SslStream(
            client.GetStream(),
            false,
            // サーバー証明書を検証する(delegateで渡す)
            new RemoteCertificateValidationCallback(ValidateServerCertificate)
        ))
        {
            sslStream.AuthenticateAsClient(
                server,
                null,
                SslProtocols.Tls13,
                checkCertificateRevocation: true
            );

            Console.WriteLine("TLS handshake completed.");
            Console.WriteLine("Negotiated protocol: " + sslStream.SslProtocol);

            string message = "Hello world";
            byte[] messageBytes = Encoding.UTF8.GetBytes(message);
            sslStream.Write(messageBytes);
            sslStream.Flush();

            byte[] buffer = new byte[1024];
            int bytesRead = sslStream.Read(buffer, 0, buffer.Length);
            string response = Encoding.UTF8.GetString(buffer, 0, bytesRead);
            Console.WriteLine("Received from server: " + response);
        }

        client.Close();
    }

    // サーバー証明書を検証する
    public static bool ValidateServerCertificate(
        object sender,
        X509Certificate certificate,
        X509Chain chain,
        SslPolicyErrors sslPolicyErrors)
    {
        X509Certificate2 cert2 = certificate as X509Certificate2;

        if (cert2 == null)
        {
            Console.WriteLine("Certificate is null.");
            return false;
        }

        // 1. OS による標準的な検証(CA署名)
        // 自己CA署名, 自己証明ではtrueにならないので 2.以下を試みる。
        if (sslPolicyErrors == SslPolicyErrors.None)
        {
            // CA署名ならばOSのチェックでここに落ちるはず
            return true; // 問題なし
        }

        // 以下、自己CA署名, 自己署名の場合
        // 2. CN と SAN の確認
        string expectedServerName = "localhost";

        // CN チェックかSANのどちらかが一致していればOK
        // ブラウザや.NET 標準 TLS 実装も OR で評価
        // .NET 標準の検証では SANを先に評価する
        // SAN チェック
        if(CheckSAN(cert2, expectedServerName))
        {
            return true;
        }

        // --- CN を正しく取り出す ---
        string cn = GetCommonName(cert2);
        if (!string.IsNullOrEmpty(cn) &&
            cn.Equals(expectedServerName, StringComparison.OrdinalIgnoreCase))
        {
            Console.WriteLine("CN matches expected server name.");
            return true;
        }

        Console.WriteLine("CN/SAN check failed.");
        return false;
    }
    
    // SANをチェックする
    static private bool CheckSAN(X509Certificate2 cert, string expectedServerName)
    {
        // 他のパターンがあったら増やす
        // 中で "dns name=" や "dNs:" のような場合も対応している
        string[] dnsHeaders = { "DNS Name=", "DNS:" };

        var sanExtension = cert.Extensions["2.5.29.17"]; // Subject Alternative Name
        if (sanExtension != null)
        {
            var asnData = new AsnEncodedData(sanExtension.Oid, sanExtension.RawData);
            string sanString = asnData.Format(false); // 改行なし

            // SAN の各 DNS 名を分割してチェック
            var entries = sanString.Split(new[] { ", " }, StringSplitOptions.RemoveEmptyEntries);
            foreach (var entry in entries)
            {
                foreach (var header in dnsHeaders)
                {
                    if (entry.StartsWith(header, StringComparison.OrdinalIgnoreCase))
                    {
                        string dns = entry.Substring(header.Length);
                        if (MatchDnsName(expectedServerName, dns))
                        {
                            return true;
                        }
                    }
                }
            }
        }
        return false;
    }

    // ワイルドカード対応の DNS 名マッチ
    private static bool MatchDnsName(string host, string pattern)
    {
        if (string.IsNullOrEmpty(host) || string.IsNullOrEmpty(pattern))
        {
            return false;
        }

        host = host.ToLowerInvariant();
        pattern = pattern.ToLowerInvariant();

        // ワイルドカード対応
        if (pattern.StartsWith("*."))
        {
            string domain = pattern.Substring(2);
            return host.EndsWith("." + domain);
        }

        return host == pattern;
    }

    private static string GetCommonName(X509Certificate2 cert)
    {
        var parser = new Org.BouncyCastle.X509.X509CertificateParser();
        var bcCert = parser.ReadCertificate(cert.RawData);
        var values = bcCert.SubjectDN.GetValueList(Org.BouncyCastle.Asn1.X509.X509Name.CN);

        // CNは複数取りえるが、実務では最初の CN(values[0])を検証対象として使用するのが一般的
        return values.Count > 0 ? values[0].ToString() : null;
    }
}

3. WireSharkでキャプチャ(終わりに)

んー。クライアントにメッセージが帰ってきているのにTLS 1.3になっていない。
これであっているのかあっていないのか。たぶんあっていないと思う。
でもなんとなく、SslStreamに流せばあとはライブラリやらOSに任せていいんだ、という気がしてきました。

image.png

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?