はじめに
- SSL通信をするに当たり、サーバー証明書が欲しくなった
- MonoのSystem.Security.X509Certificate2のp12やp7bの読出し等の挙動が怪しかった
- p7b読み込んだら空とかp12ファイル読み込んだら秘密鍵が空とか、後たまにNotImplementedが出る
- cerファイルの読み出しは問題ない
- テスト用のHTTPSサーバーが欲しくなったとき、あるいはクライアント証明書が必要になった時、c#で関連ファイルが用意できたら何かと今後に使えそうかなと思った
ということで、C#で証明書を扱う方法について調べてみた。
全て書くと長くなりすぎるので、とりあえず自己証明書の作成方法から。
前提知識
本題に入る前に、いくつかの前提知識を書いておく
X509証明書とは
Apache httpd等のHTTPサーバーでSSLを使用する際、証明書として*.cerや*.crtとしてファイルを求められる場合がある。
このバイナリ部分のデータ仕様を定義しているのがX.509という規格になる。
よく使われる用語等
専門用語がちょくちょく出てくるので、ここでよく使う単語を解説しておく
X509
証明書本体のフォーマット。証明書にはざっくりいうと以下の情報が主に入っている。
- 誰が発行したか
- 誰に対して発行したか(サーバーアドレス等)
- 有効期限
- 各種情報を元にしたハッシュ署名
「誰が」というのが発行元(Issuer)で、「誰に対して」というのが発行先(Subject)に当たる。
IssuerとSubjectが同一の場合、それは自己証明書ということになる。
自らが保持している証明書を元に、派生する証明書を作成、管理する機関を「認証局(Certificate Authority)」と呼ぶ場合がある。
自己証明書以外の証明書は全て何かの証明書の派生となるので、その派生構造はツリーとして表すことができる。
ある証明書の発行経路の証明書のどれか一つでも信頼済みのルート証明機関リストに入っている場合、
そこから派生する証明書はすべて有効とみなされる。
他にも署名、サーバー証明等目的を限定するためのフラグがあるが、オプショナルなのでなくても構わない。
拡張子は".cer"あるいは".crt"が使われることが多い。
X500 Distinguished Name(DN)
X500自体はディレクトリサービスの規格だが、その中で識別名として使われる文字列のフォーマットのこと。
OpenLDAPやActiveDirectoryサービスでよく見る"CN=..."がDistinguished Name(識別名)と呼ばれる
PKCS#12
X509証明書は単体では公開できる情報に署名を付けたものに過ぎないので、秘密鍵情報等が含まれていない。
秘密鍵とセットで証明書を扱いたい場合に使用するのがPKCS12というファイルフォーマットであり、
IISもこの形式でサーバー証明書を要求する。
外部からの不正読み取りを防ぐため、AESや3DES等の何らかの暗号化方式で暗号化されることもある。
なお、拡張子は".p12"あるいは".pfx"が使われることが多い。
PKCS#7
X509フォーマットのファイルは、あくまで一つの証明書のフォーマットになるが、実際の運用では、
その証明書本体の他に、証明機関の証明書も一つのファイルに一緒に含めたい場合がある。
もともとPKCS#7は暗号化されたデータのやり取りを証明書付きでやり取りしたい場合に
使われるフォーマットであるが、データ自体を無視して証明書コンテナのように扱うことができる。
拡張子は".p7b"あるいは".spc"が使われることが多い。
ASN.1
あるフォーマットを表現するための記法のこと。上記フォーマット達はこれで記述されている。
これだけでは実際のバイナリに落とし込むことができないので、DER等各種エンコーディング方式を使用する。
X509では大抵の場合DERが使われる。
CRL(Certificate Revocation List)
何らかのトラブル(秘密鍵流出や、署名ハッシュに脆弱性があった等)で
使えなくなった証明書リストのこと。
CRLに該当する証明書が外部から来た場合、それは無効なものとしてアプリケーションは扱う必要がある。
PEM
DER形式のファイルはバイナリなので、catで結合したり、メール添付するのが難しかったので、
テキスト形式でやり取りできるようにするためのフォーマット。
基本的にはヘッダとフッタを置いて、間にDER等のバイナリエンコーディングをbase64したものと思えば良い。
例を以下に示す
-----BEGIN CERTIFICATE-----
[base64]
-----END CERTIFICATE-----
証明書の場合はBEGIN CERTIFICATEだが、これは中身の形式によって適宜変わる。
また、暗号化等により、ヘッダ行のすぐ下にメタデータがつくこともある。
RSA
現在もっとも広く普及している非対称鍵暗号アルゴリズム。鍵長は、今は2048が主流
BouncyCastleとは
暗号化関連機能をまとめたライブラリ。
もともとはJava製だが、c#版も用意されている。
ただし、Java版の内容をほぼそのまま持ってきたものであるため、
クラスの使用方法や名前空間の指定などでやや癖がある。
使用準備
C#版の公式サイト からバイナリかソースをダウンロードして、それを参照する方法が公式に書いてある方法。
nugetから利用したい場合は、BouncyCastle.Crypto.dllあるいはPCLで欲しいならPortable.BouncyCastle が使える。
PCL版は、フル版にあるいくつかのクラスが存在しないので注意すること(特にOrg.BouncyCastle.Security.DotNetUtilities)
DotNetUtilitiesはそれほど依存関係が深くないので、coreclrで扱う分には、自分のところに丸々持ってくるという手もある。
注意として、nugetリポジトリはどちらも公式パッケージではなさそうなので、使う場合はあくまで試用程度に留めるべき。
PCLで使うこともできるので、当然coreclrで使用することも可能。
自己証明書の作成手順
既に証明機関の秘密鍵と証明書がある場合、そこから証明書を作ることが可能だが、持っていない場合、
基点となる自己証明書を作る必要がある。以下に手順を示す。
1. 秘密鍵を作成する
まず、証明書は非対称鍵が必要になる。RSA秘密鍵の作成は以下のようにする
/// 鍵オブジェクトを作成(引数はビット長)
var rsa = new RsaCryptoServiceProvider(2048)
なお、暗号化キーを生成する際に、多少時間と負荷がかかることに注意
2. 証明書に必要なパラメーターを決める
証明書には鍵ペアの他に以下のパラメーターが必要になるので、あらかじめ用意しておく
- 識別名
- 形式はX500(例: CN=abcde)
- クライアント証明書あるいは証明機関の証明書として用いる場合はあまり気にする必要はないが、サーバー証明書はCN要素がhttpsホストのアドレス部分に一致していないと、接続クライアントが不正な接続とみる場合があるので正確に設定する必要がある
- 有効期限
- RSA等の非対称鍵は、時間をかければ解けることがわかっているので、外部に公開するサーバーに関する証明書は、リスク回避のため短めが望ましい
- シリアル番号
- 証明書には、一意となるシリアル番号を付与する必要がある。少なくとも派生あるいは発行元証明書のどれとも重複してはならない
- 桁数に特に制限はないので、long型に収まらない可能性もある
3. 証明書を作成する
作成にはOrg.BouncyCastle.X509.X509V3CertificateGeneratorを使用する。具体的には以下
using System;
using System.Security.Cryptography;
using Org.BouncyCastle.X509;
using Org.BouncyCastle.Math;
using Org.BouncyCastle.Asn1.X509;
using Org.BouncyCastle.Asn1.Pkcs;
using Org.BouncyCastle.Crypto.Operators;
// Org.BouncyCastle.X509.X509CertificateとSystem.Security.Cryptographic.X509Certificates.X509Certificateの名前が重複するので、直接名前空間を使わないことにする
using MsX509Certificate2 = System.Security.Cryptography.X509Certificates.X509Certificate2;
static class X509CreateTest
{
static MsX509Certificate2 CreateSelfSignedCertificate(
// 発行元
string issuerDN,
// 非対称鍵
RSA privateKey,
// シリアル番号の文字列表現(10進数)
string serialString,
// これより前の時刻は証明書は無効
DateTime notBefore,
// これより後の時刻は証明書は無効
DateTime notAfter
)
{
var x509gen = new X509V3CertificateGenerator();
var serial = new BigInteger(serialString);
var keyPair = DotNetUtilities.GetKeyPair(privateKey);
x509gen.SetSerialNumber(serial);
x509gen.SetIssuerDN(new X509Name(issuerDN));
x509gen.SetSubjectDN(new X509Name(issuerDN));
x509gen.SetNotBefore(notBefore);
x509gen.SetNotAfter(notAfter);
x509gen.SetPublicKey(keyPair.Public);
// SHA256+RSAで署名する
var signerFactory = new Asn1SignatureFactory(PkcsObjectIdentifiers.Sha256WithRsaEncryption.Id, keyPair.Private);
var x509 = x509gen.Generate(signerFactory);
return new MsX509Certificate2(DotNetUtilities.ToX509Certificate(x509));
}
}
後はX509Certificate2.Export(X509ContentType.Cert)で得られたバイト配列をファイルなりなんなりに出力すればいい。
PEMとして出力したい場合は、Org.BouncyCastle.OpenSsl.PemWriterというヘルパークラスがある
// Org.BouncyCastle.OpenSslとSystem.IOをusingに追加
// using MsX509ContentType = System.Security.Cryptography.X509Certificates.X509ContentType; も追加
System.Security.Cryptography.X509Certificates.X509Certificate2 cert;
// certに値をセットする
// BOMはつけないようにする
using(var writer = new StreamWriter(outputFilePath,false,Encoding.ASCII))
{
var pemWriter = new PemWriter(writer);
var certdata = cert.Export(MsX509ContentType.Cert);
// 引数にOrg.BouncyCastle.X509.X509Certificateを渡した場合、証明書のPEMになる
pemWriter.WriteObject(new X509CertificateParser().ReadCertificate(certdata));
}
終りに
感想
BouncyCastle自体の使い勝手に関しては、上述のサンプルコードを見てもわかる通り、使う名前空間がやたら多いので、C#の名前空間の扱い方に慣れている身にとっては多少戸惑うこともあるかもしれない。
なので、可能な限りBouncyCastleの要素を使う箇所は一か所に封じ込め、外部からはその橋渡しのためのアセンブリ越しで使うというのがいいと思う。
ただ、機能面に関しては、暗号化に関する機能が数多く入っているのでそういう意味では充実したライブラリといえる。
PCLで使えるので、プラットフォーム間で実装の差異を可能な限り小さくしたい人向けとも言える。