1. はじめに
なんでも「とりあえず」ですが、ポート番号5000でやりとりすることを考えましょう。
サーバー側とクライアント側と二つのプログラムを作ることになります。
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の部分にドメイン名が書いてあるはずです。
そこをチェックします。
TlsClient.cs
using System;
using System.Net.Sockets;
using System.Net.Security;
using System.Security.Authentication;
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署名)
if (sslPolicyErrors == SslPolicyErrors.None)
{
// CA署名ならばOSのチェックでここに落ちるはず
return true; // 問題なし
}
// 以下、自己署名の場合
// 2. CN と SAN の確認
string expectedServerName = "localhost";
// CN チェックかSANのどちらかが一致していればOK
// ブラウザや.NET 標準 TLS 実装も OR で評価
string cn = cert2.GetNameInfo(X509NameType.DnsName, false);
if (cn.Equals(expectedServerName, StringComparison.OrdinalIgnoreCase))
{
// 予想されるサーバー名と一致しました。
Console.WriteLine("CN matches expected server name.");
return true;
}
// SAN チェック
foreach (var extension in cert2.Extensions)
{
if (extension.Oid.Value == "2.5.29.17") // Subject Alternative Name
{
var san = new System.Security.Cryptography.AsnEncodedData(extension.Oid, extension.RawData);
string sanString = san.Format(true);
if (sanString.Contains(expectedServerName))
{
Console.WriteLine("SAN contains expected server name.");
return true;
}
}
}
Console.WriteLine("CN/SAN check failed.");
return false;
}
}
3. WireSharkでキャプチャ(終わりに)
んー。クライアントには帰ってきているのにTLS 1.3になっていない。
これであっているのかあっていないのか。たぶんあっていないと思う。
でもなんとなく、SslStreamに流せばあとはライブラリやらOSに任せていいんだ、という気がしてきました。