C# SslStream で簡易HTTPSサーバを立てる

# 目的

OpenSSLを用いて秘密鍵付きの自己署名証明書を作り、C# の `SslStream` を用いて簡易HTTPSサーバを立てます。

:::note warn
この記事の目的はとりあえず適当なHTTPSサーバを立てることであり、セキュリティは考慮していません。
例えば、クラウドサービス上で秘密鍵を作る手順がありますが、実際に運用する場合は秘密鍵はローカル (サーバを動かすコンピュータ上) で生成するべきです。
:::

# 証明書の作成

OpenSSLを用い、以下の手順で秘密鍵付きの自己署名証明書を作ります。

1. 秘密鍵を作る
2. 署名要求ファイルを作る
3. 証明書を作る
4. 秘密鍵と証明書を合体する

[ブラウザでプログラミング・実行ができる「オンライン実行環境」| paiza.IO](https://paiza.io/ja)
の Bash で以下のコードを実行することで、秘密鍵付きの証明書を作成できます。

```bash
openssl genrsa -out server.key 2048
openssl req -utf8 -new -key server.key -out server.csr -subj "/C=JP/ST=Test-State/L=Test-City/O=Test/OU=/CN=localhost:8080"
openssl x509 -req -sha256 -days 365 -in server.csr -signkey server.key -out server.crt
openssl pkcs12 -export -password pass:password -inkey server.key -in server.crt -out certificate.pfx

zip -q certificate.zip server.* certificate.pfx
base64 certificate.zip
```

最初の4行で上に挙げた手順をそれぞれ実行し、最後の2行で作成したデータをBase64エンコードしたZIP形式で取り出します。
取り出したBase64データは、例えばCyberChefでデコードし、ファイル化することができます。

[From Base64 - CyberChef](https://gchq.github.io/CyberChef/#recipe=From_Base64%28%27A-Za-z0-9%2B/%3D%27,true,false%29)

デコードしたZIPファイルを展開すると、以下のファイルが得られます。

* `server.key` : 秘密鍵
* `server.csr` : 署名要求データ
* `server.crt` : 証明書
* `certificate.pfx` : 秘密鍵付きの証明書

# 簡易HTTPSサーバを立てる

C# で `SslStream` を用うと、TLS通信を行うことができます。

まず、以下のように `X509Certificate2` クラスを用いて作成した秘密鍵付きの証明書を読み込みます。
第1引数に秘密鍵付きの証明書のファイル名、第2引数に秘密鍵付きの証明書を作る時に設定したパスワードを指定します。

```cs
X509Certificate2 cert = new X509Certificate2("certificate.pfx", "password");
```

:::note
`2` が付かない `X509Certificate` を用いた場合、
後述の `AuthenticateAsServer` で以下のエラーが出てしまいました。

> System.NotSupportedException: サーバー モード SSL は関連付けられた秘密キーを使用した証明書を使用しなければなりません。
:::

通常のTCPサーバを立て、接続されたら、以下のように `SslStream` を作成し、サーバとして初期化します。

```cs
SslStream stream = new SslStream(client.GetStream(), false);
stream.AuthenticateAsServer(cert, false, SslProtocols.Tls12 | SslProtocols.Tls13, false);
```

:::note
TLSのバージョンを明示的に指定しないと、TLSv1になってしまい、Google Chromeで接続できませんでした。
:::

以降、この `stream` を通じてクライアントとの通信ができます。

以下がコード全体です。

```cs:SslServerTest.cs
using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Net.Security;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;

namespace Test
{
	public class SslServerTest
	{
		public static void Main(string[] args)
		{
			X509Certificate2 cert = new X509Certificate2("certificate.pfx", "password");
			TcpListener listener = new TcpListener(IPAddress.Any, 8080);
			listener.Start();
			for(;;)
			{
				Console.WriteLine("listening");
				TcpClient client = listener.AcceptTcpClient();
				Console.WriteLine("accepted");
				SslStream stream = new SslStream(client.GetStream(), false);
				try
				{
					stream.AuthenticateAsServer(cert, false, SslProtocols.Tls12 | SslProtocols.Tls13, false);
					StreamReader sr = new StreamReader(stream);
					string line;
					while ((line = sr.ReadLine()) != null && !line.Equals(""))
					{
						Console.WriteLine("received: " + line);
					}
					StreamWriter sw = new StreamWriter(stream);
					sw.Write("HTTP/1.0 200 OK\r\n");
					sw.Write("Conenction: close\r\n");
					sw.Write("Content-Type: text/plain\r\n");
					sw.Write("Content-Length: 5\r\n");
					sw.Write("\r\n");
					sw.Write("hello");
					sw.Flush();
				}
				catch (Exception e)
				{
					Console.WriteLine(e);
				}
				finally
				{
					stream.Close();
				}
			}
		}
	}
}
```

このコードを、最近のWindowsに標準付属しているC#コンパイラでコンパイルします。

```
"C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc" SslServerTest.cs
```

コンパイルした結果の実行ファイルを実行すると、Google Chrome で接続することができました。

![Google Chrome で接続した様子](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/bbb683fa-ef6f-f74c-b722-4fe9055ce19c.png)

# 参考サイト

## 証明書の作成

* [【初心者向け】最速で自己証明書を発行するためのコマンド集 - サーバーワークスエンジニアブログ](https://blog.serverworks.co.jp/make-self-cert)
* [opensslコマンドでcsrファイルを作成する際のサーバ名等をコマンドラインオプションで指定する | パソコン鳥のブログ](https://pcvogel.sarakura.net/2015/03/24/31434)
* [秘密鍵、公開鍵形式からPKCS#12（pfx）形式への変換方法](https://knowledge.digicert.com/ja/jp/solution/SO23603.html)
* [opensslのパスフレーズ引数について - Qiita](https://qiita.com/takuyaWt/items/300cbb0570814b521732)

## 簡易HTTPSサーバを立てる

* [X509Certificate2 Class (System.Security.Cryptography.X509Certificates) | Microsoft Learn](https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.x509certificates.x509certificate2?view=net-5.0)
* [SslStream Class (System.Net.Security) | Microsoft Learn](https://learn.microsoft.com/en-us/dotnet/api/system.net.security.sslstream?redirectedfrom=MSDN&view=net-5.0)
* [SslStream.AuthenticateAsServer Method (System.Net.Security) | Microsoft Learn](https://learn.microsoft.com/en-us/dotnet/api/system.net.security.sslstream.authenticateasserver?view=net-5.0#system-net-security-sslstream-authenticateasserver(system-security-cryptography-x509certificates-x509certificate-system-boolean-system-security-authentication-sslprotocols-system-boolean))
* [SslProtocols Enum (System.Security.Authentication) | Microsoft Learn](https://learn.microsoft.com/en-us/dotnet/api/system.security.authentication.sslprotocols?view=net-5.0)
* [OSに標準付属のC#／VBコンパイラーでソースコードをコンパイルするには？：.NET TIPS - ＠IT](https://atmarkit.itmedia.co.jp/ait/articles/1504/15/news019.html)
* [c# - How to implement a Https web server using SslStream and a self signed certificate? - Stack Overflow](https://stackoverflow.com/questions/30618883/how-to-implement-a-https-web-server-using-sslstream-and-a-self-signed-certificat)
* [c# - SslStream AuthenticateAsServer failed with "Server certificate Private Key unavailable." exception on Mono - Stack Overflow](https://stackoverflow.com/questions/26672145/sslstream-authenticateasserver-failed-with-server-certificate-private-key-unava)
* [c# - Associate a private key with the X509Certificate2 class in .net - Stack Overflow](https://stackoverflow.com/questions/18462064/associate-a-private-key-with-the-x509certificate2-class-in-net)