C# gRPC v1.16.0 SSLに関する仕様変更

## はじめに ##

C# gRPC v1.16.0 で SSL に関する仕様変更が行われ、クライアント資格証明の要求と照合に関する動作を細かく指定できるようになりました。

## SslServerCredentials クラスの変更 ##

### コンストラクタの追加 ###

SslClientCertificateRequestType 列挙体を受け取るコンストラクタが追加されました。

```c#:SslServerCredentials

// 従来のコンストラクタ
public SslServerCredentials(
    IEnumerable<KeyCertificatePair> keyCertificatePairs
    , string rootCertificates
    , bool forceClientAuth
);

// SslClientCertificateRequestType 列挙体を受け取るコンストラクタが追加されました。
public SslServerCredentials(
    IEnumerable<KeyCertificatePair> keyCertificatePairs
    , string rootCertificates
    , SslClientCertificateRequestType clientCertificateRequest
);
```

### 従来バージョンとの比較 ###

SslServerCredentials クラスの従来のコンストラクタ（bool を受け取るコンストラクタ）は、true が指定された場合は RequestAndRequireAndVerify と見なし、false が指定された場合は DontRequest と見なすように実装されています。
従来のコンストラクタによる gRPC.Core 内部の挙動に誤りがあり、それを修正するとともに、挙動を制御しているオプション値を引数とするコンストラクタを公開したというのが実際のところのようです。

| bool | SslClientCertificateRequestType | 動作 |
|:--|:--|:--|
| false | DontRequest | クライアント資格情報を要求しない。 |
| - | RequestButDontVerify | クライアント資格情報は強制しない。提示されてもルート証明書の照合は行わない。 |
| - | RequestAndVerify | クライアント資格情報は強制しない。提示された場合はルート証明書を照合する。 |
| - | RequestAndRequireButDontVerify | クライアント資格情報を要求するが、ルート証明書の照合は行わない。 |
| true | RequestAndRequireAndVerify | クライアント証明情報を要求し、ルート証明書を照合する。 |


## 実装例 ##

```C#:サーバーサイドの実装

// 資格情報を指定してポートを登録します。
Server server = new Server();
server.Ports.Add("127.0.0.1", 50001, GetServerCredentials());

/// <summary>
/// サーバー資格情報を取得します。
/// </summary>
/// <returns></returns>
private ServerCredentials GetServerCredentials()
{
    string rootCert = File.ReadAllText("testCa.crt");
    string serverCert = File.ReadAllText("testServer.crt");
    string serverKey = File.ReadAllText("testServer.key");

    KeyCertificatePair keypair = new KeyCertificatePair(serverCert, serverKey);

    SslServerCredentials credentials = new SslServerCredentials(
        new KeyCertificatePair[] { keypair }
        , rootCert
        // クライアント資格情報の要求動作を指定します
        , SslClientCertificateRequestType.RequestAndRequireAndVerify
    );

    return credentials;
}
```

```C#:クライアントサイドの実装

// 資格情報を指定してチャネルを生成します。
Channel = new Channel("127.0.0.1:50001"), GetClientCredentials());

/// <summary>
/// クライアント資格情報を取得します。
/// </summary>
/// <returns></returns>
private ChannelCredentials GetClientCredentials()
{
    string rootCert = File.ReadAllText("testCa.crt");
    string clientCert = File.ReadAllText("testClient.crt");
    string clientKey = File.ReadAllText("testClient.key");

    KeyCertificatePair keypair = new KeyCertificatePair(clientCert, clientKey);

    SslCredentials credentials = new SslCredentials(rootCert, keypair);

    return credentials;
}
```

## 動作確認 ##

次の証明書ファイルを準備しました。すべて OpenSSL を使って作成した自己証明書です。

| ファイル | 説明 |
|:--|:--|
| testCa.crt  | ルートCA証明書です。 |
| testServer.crt  | サーバー証明書です。testCa.crt をルート認証局に指定しています。 |
| testServer.key  | testServer.crt の秘密鍵です。  |
| testClient.crt  | クライアント証明書です。testCa.crt をルート認証局に指定しています。 |
| testClient.key  | testClient.crt の秘密鍵です。 |
| otherClient.crt  | ルート認証局が異なるクライアント証明書です。 |
| otherClient.key  | otherClient.crt の秘密鍵です。 |


### DontRequest の場合 ###

```C#

string rootCert = File.ReadAllText("testCa.crt");
string clientCert = File.ReadAllText("testClient.crt");
string clientKey = File.ReadAllText("testClient.key");
string otherCert = File.ReadAllText("otherClient.crt");
string otherKey = File.ReadAllText("otherClient.key");

SslCredentials credentials = new SslCredentials(rootCert);
==> OK

SslCredentials credentials = new SslCredentials(rootCert, new KeyCertificatePair(clientCert, clientKey);
==> OK

SslCredentials credentials = new SslCredentials(rootCert, new KeyCertificatePair(otherCert, otherKey);
==> OK
```

### RequestButDontVerify の場合 ###

```C#

SslCredentials credentials = new SslCredentials(rootCert);
==> OK

SslCredentials credentials = new SslCredentials(rootCert, new KeyCertificatePair(clientCert, clientKey);
==> OK

SslCredentials credentials = new SslCredentials(rootCert, new KeyCertificatePair(otherCert, otherKey);
==> OK
```

### RequestAndVerify の場合 ###

```C#

SslCredentials credentials = new SslCredentials(rootCert);
==> OK

SslCredentials credentials = new SslCredentials(rootCert, new KeyCertificatePair(clientCert, clientKey);
==> OK

SslCredentials credentials = new SslCredentials(rootCert, new KeyCertificatePair(otherCert, otherKey);
==> NG
```

### RequestAndRequireButDontVerify の場合 ###

```C#

SslCredentials credentials = new SslCredentials(rootCert);
==> NG

SslCredentials credentials = new SslCredentials(rootCert, new KeyCertificatePair(clientCert, clientKey);
==> OK

SslCredentials credentials = new SslCredentials(rootCert, new KeyCertificatePair(otherCert, otherKey);
==> OK
```

### RequestAndRequireAndVerify の場合 ###

```C#

SslCredentials credentials = new SslCredentials(rootCert);
==> NG

SslCredentials credentials = new SslCredentials(rootCert, new KeyCertificatePair(clientCert, clientKey);
==> OK

SslCredentials credentials = new SslCredentials(rootCert, new KeyCertificatePair(otherCert, otherKey);
==> NG
```

## 補足 ##

### クライアント資格情報によって認証されているかどうか ###

クライアント資格情報によって認証されているかどうかをサーバーサイドで判定するには、RPC メソッドの引数で渡される ServerCallContext オブジェクトの内容を確認します。
認証されている場合、ServerCallContext.AuthContext.IsPeerAuthenticated プロパティの値が true になり、ServerCallContext.AuthContext.PeerIdentity プロパティに認証情報が格納されます。ルート証明書を照合したかどうかは分からないようです。


## おわりに ##

環境が手元にないため確認していませんが gRPC 1.16.0 では Linux 上での SLL 処理のスループット向上が図られているなど、SSL に対する強化や改善は継続的に行われていくと思われます。