銀の光と碧い空

クラウドなインフラとC#なアプリ開発の狭間にいるエンジニアの日々

ASP.NET/IIS 上で X509Certificate2 をバイト配列指定で生成するときは、 Application Pool の実行ユーザーのプロファイルを読みこませないといけません

C# Advent Calendar と ASP.NET Advent Calendar の9日目のエントリです。

もともと別の内容を書くつもりだったのですが、仕事であまりにはまったことがあったので、それを書こうと思います。

背景

Google API をたたきたい状況で、APIサーバー側(つまりGoogle側)で発行される証明書を使って、 X509Certificates2 クラスのインスタンスを生成して認証する必要があります。

X509Certificate2 クラス (System.Security.Cryptography.X509Certificates)

証明書ファイルのパスを指定してインスタンスを生成することもできますが、

X509Certificate2 コンストラクター (String, String, X509KeyStorageFlags) (System.Security.Cryptography.X509Certificates)

証明書ファイルのコンテンツを byte配列として渡すこともできます。

X509Certificate2 コンストラクター (Byte[], String, X509KeyStorageFlags) (System.Security.Cryptography.X509Certificates)

どういうときにbyte配列を指定したいかというと、複数のWebサーバーで同じ証明書を使うときなどにネットワーク上のストレージに証明書を置いて管理したいときになります*1

ネットワーク越しに証明を取得するコード

AWS SDK を使って、S3から証明書を取得してX509Certificates2 クラスのインスタンスを生成するコードはこんな感じになります。

var s3 = AWSClientFactory.CreateAmazonS3Client(s3Key, s3Secret, RegionEndpoint.APNortheast1);
var s3Obj = s3.GetObject(certficateBucket, certificateKey);

byte[] bytes = null;
using (var responseStream = s3Obj.ResponseStream)
{
    bytes = responseStream.ReadFully();
}

var certificate = new X509Certificate2(bytes, certificatePassword, X509KeyStorageFlags.Exportable);

//以下の拡張メソッドを用意
public static class StreamExtension
{
    public static byte[] ReadFully(this Stream input)
    {
        var buffer = new byte[16 * 1024];
        using (var ms = new MemoryStream())
        {
            int read;
            while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
            {
                ms.Write(buffer, 0, read);
            }
            return ms.ToArray();
        }
    }
}

このようなコードを Global.asax.csApplication_Start メソッドの中から呼び出していました。

起きたこと

で、このコードを載せたアプリをIISで動かすとエラーが発生しました。(以下のエラーはエラーの詳細を表示する設定を Web.config に指定している場合にブラウザ上に表示されるメッセージの一部)

[CryptographicException: 指定されたファイルが見つかりません。]
   System.Security.Cryptography.CryptographicException.ThrowCryptographicException(Int32 hr) +41
   System.Security.Cryptography.X509Certificates.X509Utils._LoadCertFromBlob(Byte[] rawData, IntPtr password, UInt32 dwFlags, Boolean persistKeySet, SafeCertContextHandle& pCertCtx) +0
   System.Security.Cryptography.X509Certificates.X509Certificate.LoadCertificateFromBlob(Byte[] rawData, Object password, X509KeyStorageFlags keyStorageFlags) +313
   System.Security.Cryptography.X509Certificates.X509Certificate2..ctor(Byte[] rawData, String password, X509KeyStorageFlags keyStorageFlags) +98

このエラー、ブラウザ上に表示しないようにしていると、イベントログにこんな感じに表示されるだけで、さらに謎になります*2

(一部抜粋)

障害が発生しているアプリケーション名: w3wp.exe、バージョン: 8.0.9200.16384
障害が発生しているモジュール名: ntdll.dll、バージョン: 6.2.9200.16579
例外コード: 0xc0000374
障害オフセット: 0x00000000000ebd59

障害が発生しているアプリケーション パス: c:\windows\system32\inetsrv\w3wp.exe
障害が発生しているモジュール パス: C:\Windows\SYSTEM32\ntdll.dll

障害が発生しているパッケージの完全な名前: 
障害が発生しているパッケージに関連するアプリケーション ID: 

なぜ、 byte配列を指定しているのにファイルが見つからないといわれるのか...

原因と対策

いろいろぐぐっていたら似たようなことにはまった人がStackOverflowにいて自己解決していました。

どうやら、X509Certificate2 を byte配列を指定してインスタンス化しようとすると内部でユーザープロファイルに書きこみ処理を行っているようで、IIS でApplication Poolの実行ユーザーに対してユーザープロファイルの読みこみを指定していないと書きこむことができなくてエラーになるようです。 というわけで、ユーザープロファイルを読みこむようにしてみましょう。以下のサイトがわかりやすいです。

ApplicationHost.config で次のように設定すればOKです(他のIdentityType でもいけるはず)。

<system.applicationHost>
    <applicationPools>
        <add name="MyAppPool">
            <processModel identityType="SpecificUser" userName="MyAppPoolUser" password="password" />
        </add>
    </applicationPools>
    <!--省略-->
</system.applicationHost>

この設定をすると無事最初のコードで X509Certificates2をインスタンス化して、認証に使うことができました。めでたし、めでたし。 なお、ユーザープロファイルのディレクトリを見ても、この証明書ファイルとして保存しているわけではないようです。

なお、もともとC# Advent Calendarで書く予定だった社内ライブライのビルド・デプロイ戦略については改めて書きたいと思っています。

*1:S3に証明書を置いて、その証明書を取得できる権限を IAM Role で制御するなど

*2:例外コードでぐぐったり、VSでプロセスにアタッチしたりすると「ハンドルされない例外が 0x000007FD14B4BD59 (ntdll.dll) で発生しました(w3wp.exe 内): 0xC0000374: ヒープは壊れています。 (パラメーター: 0x000007FD14BA05F0)。」とかいうメッセージが出るのもさらに謎を深める