1. はじめに
なんでも「とりあえず」ですが、ポート番号5000でやりとりすることを考えましょう。
サーバー側とクライアント側と二つのプログラムを作ることになります。
ChatGPTの出力が元になっています。利用される方は自己責任で。
2-1.サーバー側のコード
- サーバーは 自分で作ったサーバー証明書をロードし、クライアントを待ちます
- クライアントが接続し、クライアントが何か送信してきたら、それを返します
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.(とりあえず)クライアント側のコード
- クライアントはサーバーに接続します
- サーバーを検証することは考えずに問答無用で認証します(受け入れます)
- サーバーに "Hello, world!" を送信します
- サーバーから送信された文字列を表示します
この場合、送った "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に任せていいんだ、という気がしてきました。