Unity(YetAnotherHttpHandler)とMagicOnionで自己署名証明書を使ったHTTPS&HTTP/2通信
概要
MagicOnion + MessagePack + YetAnotherHttpHandler でリアルタイム通信を行う の続編です。
自己署名証明書を使って Unity(YetAnotherHttpClient)とリアルタイムサーバー(MagicOnion)で HTTPS & HTTP/2 通信する手順を紹介します。
基本はConfigure SSL/TLS | MagicOnion の通りですが、YetAnotherHttpHandler を使ったり、最新の dotnet8 を使ったりする場合は、手順が変わります。
クライアントの HTTPS&HTTP/2 クライアントとして YetAnotherHttpHandler を利用しています。YetAnotherHttpHandler は RootCertificates
を指定することで自己署名証明書を内部で検証してくれます。YetAnotherHttpHandler #Using custom root certificates - GitHub
また、https 通信のフローについては 【図解】よく分かるデジタル証明書(SSL証明書)の仕組み 〜https通信フロー,発行手順,CSR,自己署名(オレオレ)証明書,ルート証明書,中間証明書の必要性や扱いについて〜 | SEの道標 が分かり易いです。
環境
開発環境
- Windows 11
- WSL 2
- WSL のインストール | Microsoft Learnが参考になります。
- Ubuntu 20.04
- WSL の Linux ディストリビューションとして上記を使っていましたが、証明書を生成するための OpenSSL 3 系が使える Linux ディストリビューションであればなんでもよいです。
主な使用ライブラリ
-
YetAnotherHttpHandler 1.0.0
- Unity で利用できる HTTP/2 クライアント
-
MagicOnion 6.0.1
- HTTP/2 サーバーとして利用できる!
-
OpenSSL 3.2.0
- 証明書を発行できるツール
- OpenSSL 3 系である必要があります
- YetAnotherHttpHandler は OpenSSL 1 系で生成した証明書は信頼しません。
手順
MagicOnion Server と Unity Client の準備
以下に本記事で紹介するサンプルリポジトリを置いておきます。記事の説明が不十分だったりする場合はこちらを参考にしてください。
- MagicOnion Server
- Unity Client
(必要な人は) Ubuntu 20.04 で OpenSSL 3 系へ更新
YetAnotherHttpHandler の証明書検証で成功するためには OpenSSL 3 系で生成された証明書である必要があります。
OpenSSL 1.1.1 で生成した証明書は YetAntherHttpHandler の証明書検証時のバージョン検証で失敗してしまいます。
また、Ubuntu 20.04 の場合は、デフォルトで OpenSSL1.1.1 がインストールされているので OpenSSL 3 系に更新してから証明書を生成する必要があります。Ubuntu 20.04 で OpenSSL 3.2.0 に更新する が参考になります。
以下のように OpenSSL のバージョンが 3.2.0 以上であれば OK です。
$ openssl version
OpenSSL 3.2.0 23 Nov 2023 (Library: OpenSSL 3.2.0 23 Nov 2023)
自己署名証明書の生成
秘密鍵 (server.key) と 証明書発行要求(server.csr)、証明書 (server.crt) と 証明書と対応する秘密鍵を含むパッケージ (server.pfx) を生成します。
.gitignore
に少なくとも*.key
と*.pfx
は追加しておきましょう
例えば、Ubuntu
へbash
でログインしmagiconion-sample-server
に移動しServer/certificate
配下に必要なファイルを置くとします。
秘密鍵の生成
秘密鍵を生成します。このファイルが流出すると不正アクセスが可能になってしまうので、適切に保管しましょう。
# path-to magiconion-sample-server
mkdir Server/certificate
cd Server/certificate
openssl genrsa 4096 > server.key
証明書発行要求の生成
証明書発行要求(CSR)は、認証局が証明書を生成するのに必要なファイルで、サーバーホスト名や秘密鍵(server.key)に対応する公開鍵が含まれます。
ホスト名は例えばdummy.example.com
とした場合以下のような設定ファイルを用意し、この設定ファイルをもとに CSR を生成します。
[ req ]
default_bits = 4096
default_md = sha256
prompt = no
encrypt_key = no
distinguished_name = dn
[ dn ]
CN=dummy.example.com
[ ext ]
basicConstraints=CA:FALSE
keyUsage=digitalSignature, keyEncipherment
subjectAltName=@alt_names
[ alt_names ]
DNS.1=dummy.example.com
DNS.2=www.dummy.example.com
openssl req -new -key server.key -out server.csr -config myssl.cnf
自己署名証明書の生成
今回は自己署名証明書を生成するので、認証局の代わりが自身になります。
そのため CSR 生成に使った同じ秘密鍵 (server.key) で CSR をハッシュ化し、CSR の末尾にハッシュを追記する(署名する)ことで自己署名証明書を生成します。
CSR から秘密鍵で署名して証明書を生成するコマンドは以下になります。
openssl x509 -req -in server.csr -signkey server.key -out server.crt -days 7300 -extfile myssl.cnf -extensions ext
上記のコマンドでmyssl.cnf
を指定しているのは、ext
やalt_names
を指定するためです。
openssl req -text -noout -verify -in server.csr
とopenssl x509 -in server.crt -text -noout
を見比べてみると違いが判ります。
サーバー用 pfx ファイルの生成
証明書と対応する秘密鍵を含むパッケージ (server.pfx) を生成します。サーバーではこのファイルを読み込みます。
生成時にパスワードを設定することができます。こちらも server.key
と同様に適切に保管しましょう。
openssl pkcs12 -export -out server.pfx -inkey server.key -in server.crt
証明書の確認
証明書の中身を確認します。
openssl x509 -in server.crt -text -noout
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
48:9a:ec:0f:5f:af:7d:6a:69:b0:d4:57:1e:a2:41:13:57:fe:62:e9
Signature Algorithm: sha256WithRSAEncryption
Issuer: CN=dummy.example.com
Validity
Not Before: Jan 8 10:31:28 2024 GMT
Not After : Jan 7 10:31:28 2025 GMT
Subject: CN=dummy.example.com
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (4096 bit)
Modulus:
00:9b:16:ad:a6:1f:11:fc:c4:7a:77:46:49:6b:0d:
...(省略)
2d:8d:f9
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Basic Constraints:
CA:FALSE
X509v3 Key Usage:
Digital Signature, Key Encipherment
X509v3 Subject Alternative Name:
DNS:dummy.example.com, DNS:www.dummy.example.com
X509v3 Subject Key Identifier:
A4:A8:BA:AF:16:C7:00:FE:98:A2:B8:10:9B:B9:77:60:15:2D:5B:3E
Signature Algorithm: sha256WithRSAEncryption
Signature Value:
4e:90:0f:e9:0a:f6:1b:24:72:44:b1:be:07:95:62:e4:2f:b9:
...(省略)
f7:21:e1:74:e5:e7:b9:9c
以下が確認できれば OK です。ここら辺が欠けていると YetAnotherHttpHandler での証明書検証が失敗しています。
Version: 3
- 自己署名証明書なので、
Issuer
とSubject
が一致している -
CA:FALSE
となっており、認証局ではなくサーバー用の証明書を示している - Subject Alternative Name (SAN) が設定されている
- YetAnotherHttpHandler はアクセスしようとする FQDN と SAN が一致しているかを検証しているっぽい
Windows のホストファイルを編集
Windowsローカル環境で dummy.example.com
のホスト名で証明書を発行したので、そのホスト名と IP アドレスをマッピングをします。
hosts
ファイルで指定したマッピングは DNS サーバーによるホスト名の解決よりも優先されます。
C:\Windows\System32\drivers\etc\hosts
ファイルメモ帳などで管理者権限で開き、以下の行を追記します。
127.0.0.1 dummy.example.com
これによって、dummy.example.com
にアクセスしようとすると127.0.0.1
に接続するようになります。
サーバーの https 設定
2通りあります。一つはサーバー起動時に自動で読み込まれるappsettings.json
を利用する方法です。もう一つは一つはコード上( Program.cs
)にエンドポイントを記述する方法です。
参考 : Kestrel web server in ASP.NET Core | Microsoft Learn
方法1:appsettings.json で https の設定
以下を Server プロジェクトのルートディレクトリに置きます。
{
"Kestrel": {
"Endpoints": {
"Grpc": {
"Url": "http://dummy.example.com:5000",
"Protocols": "Http2"
},
"Https": {
"Url": "https://dummy.example.com:5001",
"Protocols": "Http1AndHttp2"
},
"Http": {
"Url": "http://dummy.example.com:5002",
"Protocols": "Http1"
}
},
"Certificates": {
"Default": {
"Path": "./certificate/server.pfx",
"Password": "password"
}
}
}
}
方法2:コード上での https の設定
以下のように記述します。
~~~
builder.WebHost.ConfigureKestrel(options =>
{
// HTTP/2 Only のエンドポイント(not HTTPS)
options.Listen(IPAddress.Parse("0.0.0.0"), 5000,
listenOptions => { listenOptions.Protocols = HttpProtocols.Http2; });
// HTTP/2 ,HTTPS エンドポイントの設定
options.Listen(IPAddress.Parse("0.0.0.0"), 5001, listenOptions =>
{
// --load-cert=true が指定されていたら証明書を読み込む
if (args.Any(arg => arg == "--load-cert=true"))
{
Console.WriteLine("load certificate");
listenOptions.UseHttps(new X509Certificate2("certificate/server.pfx", "password"));
listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
}
});
// 疎通確認用のHTTP 1.1エンドポイントの設定
options.Listen(IPAddress.Parse("0.0.0.0"), 5002,
listenOptions => { listenOptions.Protocols = HttpProtocols.Http1; });
});
~~~
上記の 0.0.0.0
は127.0.0.1
でも大丈夫です。
サーバープロジェクトの設定
続いてappsettings.json
やserver.pfx
をサーバー実行ファイルが存在するディレクトリにコピーする設定を追加します。
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<!-- FOR SSL/TLS SUPPORT -->
<None Update="certificate/server.pfx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
これでサーバー側の設定は終了です。
Unity Client の設定
証明書の配置
Unity Editor を開き、Projects タブからAssets/StreamingAssets
を作成し、 certificate/server.crt
を置きます。
handler での証明書の指定
YetAnotherHttpHandler の RootCertificates
に生成した自己署名証明書を指定します。
~~~
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
public static void OnRuntimeInitialize()
{
GrpcChannelProviderHost.Initialize(
new GrpcNetClientGrpcChannelProvider(() => new GrpcChannelOptions()
{
HttpHandler = new Cysharp.Net.Http.YetAnotherHttpHandler()
{
//Http2Only = true,
RootCertificates =
File.ReadAllText(Path.Combine(Application.streamingAssetsPath, "certificate/server.crt")),
SkipCertificateVerification = false
}
}));
}
~~~
接続先として、サーバーで設定したアドレスを指定します。
~~~
_channel = GrpcChannelx.ForAddress("https://dummy.example.com:5001/");
~~~
また、http2 only (not https) で接続したい場合は、Http2Only
のコメントアウトを外し、RootCertificate
、SkipCertificateVerification
をコメントアウトします。この場合、接続先はhttp://dummy.example.com:5000/
になります。
動かす
Serverアプリを起動します。IDEから起動するなりビルド後の実行ファイルを起動するなりしてください。
https エンドポイントをコード上に定義した場合は、--load-cert=true
を引数に指定して実行します。
クライアント側もエディターから実行するなり、ビルド後の実行ファイルを起動するなりしてください。
Unity で IL2CPPビルドをする場合は以下が参考になります。
おわりに
安全な通信ができるようになって嬉しい!
ただ、https 通信ができるようになったとはいえ、サーバー側でクライアント認証をしているわけではないので、どんなクライアントでも サーバーに接続できてしまうので注意が必要です。MagicOnion Server には Filter の仕組みがあり、これをつかって認証トークンを持っていないリクエストを弾くみたいなことができるはずです。
需要がありそうなら MagicOnion ネタでもうしばらく記事を書いていけたらいいなぁ~と思います。
- MagicOnion + MessagePack + YetAnotherHttpHandler でリアルタイム通信を行う
- Unity(YetAnotherHttpHandler)とMagicOnionで自己署名証明書を使ったHTTPS&HTTP/2通信(本記事)
- MagicOnion Server コンテナを GCP にデプロイする
- DFrame で MagicOnion Server の負荷試験
- TLS 終端用リバースプロキシ追加によるサーバーパフォーマンス差
Discussion