🙌

Unity(YetAnotherHttpHandler)とMagicOnionで自己署名証明書を使ったHTTPS&HTTP/2通信

2024/01/14に公開

概要

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 の準備

以下に本記事で紹介するサンプルリポジトリを置いておきます。記事の説明が不十分だったりする場合はこちらを参考にしてください。

(必要な人は) 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は追加しておきましょう

例えば、Ubuntubashでログインしmagiconion-sample-server に移動しServer/certificate配下に必要なファイルを置くとします。

秘密鍵の生成

秘密鍵を生成します。このファイルが流出すると不正アクセスが可能になってしまうので、適切に保管しましょう。

bash
# path-to magiconion-sample-server
mkdir Server/certificate
cd Server/certificate
openssl genrsa 4096 > server.key

証明書発行要求の生成

証明書発行要求(CSR)は、認証局が証明書を生成するのに必要なファイルで、サーバーホスト名や秘密鍵(server.key)に対応する公開鍵が含まれます。

ホスト名は例えばdummy.example.comとした場合以下のような設定ファイルを用意し、この設定ファイルをもとに CSR を生成します。

myssl.cnf
[ 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 から秘密鍵で署名して証明書を生成するコマンドは以下になります。

bash
openssl x509 -req -in server.csr -signkey server.key -out server.crt -days 7300 -extfile myssl.cnf -extensions ext

上記のコマンドでmyssl.cnfを指定しているのは、extalt_namesを指定するためです。
openssl req -text -noout -verify -in server.csropenssl x509 -in server.crt -text -nooutを見比べてみると違いが判ります。

サーバー用 pfx ファイルの生成

証明書と対応する秘密鍵を含むパッケージ (server.pfx) を生成します。サーバーではこのファイルを読み込みます。
生成時にパスワードを設定することができます。こちらも server.keyと同様に適切に保管しましょう。

bash
openssl pkcs12 -export -out server.pfx -inkey server.key -in server.crt

証明書の確認

証明書の中身を確認します。

bash
 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
  • 自己署名証明書なので、IssuerSubjectが一致している
  • 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 プロジェクトのルートディレクトリに置きます。

appsettings.json
{
  "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"
      }
    }
  }
}

https://github.com/tou-tou/magiconion-sample-server/blob/e007c6bb770fc46711378b07c1b5b6ff142b0008/Server/appsettings.json

方法2:コード上での https の設定

以下のように記述します。

Program.cs
~~~
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; });
});
~~~

https://github.com/tou-tou/magiconion-sample-server/blob/e007c6bb770fc46711378b07c1b5b6ff142b0008/Server/Program.cs

上記の 0.0.0.0127.0.0.1でも大丈夫です。

サーバープロジェクトの設定

続いてappsettings.jsonserver.pfxをサーバー実行ファイルが存在するディレクトリにコピーする設定を追加します。

Server.csproj
    <ItemGroup>
        <None Update="appsettings.json">
            <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
        </None>
        <!-- FOR SSL/TLS SUPPORT -->
        <None Update="certificate/server.pfx">
            <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
        </None>
    </ItemGroup>

https://github.com/tou-tou/magiconion-sample-server/blob/e007c6bb770fc46711378b07c1b5b6ff142b0008/Server/Server.csproj

これでサーバー側の設定は終了です。

Unity Client の設定

証明書の配置

Unity Editor を開き、Projects タブからAssets/StreamingAssetsを作成し、 certificate/server.crt を置きます。

handler での証明書の指定

YetAnotherHttpHandler の RootCertificates に生成した自己署名証明書を指定します。

Initializer.cs
~~~
[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
                    }
                }));
        }
~~~

接続先として、サーバーで設定したアドレスを指定します。

Program.cs
~~~
 _channel = GrpcChannelx.ForAddress("https://dummy.example.com:5001/");
~~~

また、http2 only (not https) で接続したい場合は、Http2Onlyのコメントアウトを外し、RootCertificateSkipCertificateVerificationをコメントアウトします。この場合、接続先はhttp://dummy.example.com:5000/になります。

動かす

Serverアプリを起動します。IDEから起動するなりビルド後の実行ファイルを起動するなりしてください。
https エンドポイントをコード上に定義した場合は、--load-cert=trueを引数に指定して実行します。

クライアント側もエディターから実行するなり、ビルド後の実行ファイルを起動するなりしてください。

Unity で IL2CPPビルドをする場合は以下が参考になります。
https://zenn.dev/toutou/articles/7918da3d1a9e1d#il2cpp-ビルドの設定

おわりに

安全な通信ができるようになって嬉しい!
ただ、https 通信ができるようになったとはいえ、サーバー側でクライアント認証をしているわけではないので、どんなクライアントでも サーバーに接続できてしまうので注意が必要です。MagicOnion Server には Filter の仕組みがあり、これをつかって認証トークンを持っていないリクエストを弾くみたいなことができるはずです。

需要がありそうなら MagicOnion ネタでもうしばらく記事を書いていけたらいいなぁ~と思います。

参考

GitHubで編集を提案

Discussion