Edited at

BouncyCastle(C#)で他の証明書から派生したX509証明書を作成する

More than 3 years have passed since last update.


はじめに

X509自己証明書の作成の記事を読んでいることが前提。

上記記事では自己証明書を作成する手順を書いたが、普通証明書というのは、ルート証明書を除いて何かの証明書の派生になる。

そこで、今回は証明書署名要求を元に、認証局がx509証明書を作るという手順を書いていく。


認証局用証明書を作成する

既に他の証明機関から証明書をもらっている場合以外は、自己証明書を作成し、これを認証局用の証明書とする必要がある。

要領は上記記事とほとんど同じだが、他の証明書を派生させる場合、自己証明書には以下の三つの拡張を追加する必要がある。


基本制約(Basic Constraints)

基本制約とは、その証明書から他の証明書を作ることを許可するかしないかのフラグのようなもので、

更に許可した場合には、何階層まで許可するかまで設定できる。

BouncyCastleでこの拡張を付与したい場合は、以下のようなコードを作成時に加える。

// using Org.BouncyCastle.Asn1.X509

// bool isCAは、派生を許可するならばtrue,しないならばfalse。階層は指定なしならば無限
var basicConst = new BasicConstraints(isCA);
var x509ext = new X509Extension(true,new DerOctetString(basicConst));


サブジェクト鍵識別子(SubjectKeyIdentifier)

どの公開鍵に対して発行したかという情報を付加するための拡張。

BouncyCastleでは、以下のような手順で作成を行う

// using Org.BouncyCastle.X509.Extension;

// using Org.BouncyCastle.Asn1;
// using Org.BouncyCastle.Crypto;
// AsymmetricCipherKeyPair keyPair;
// 公開鍵のみ必要で、秘密鍵は必要ない
var subkey = new SubjectKeyIdentifierStructure(keyPair.Public);
var x509ext = new X509Extension(false,new DerOctetString(subkey));


認証機関鍵識別子(AuthorityKeyIdentifier)

認証局のどの公開鍵を使用して署名したかという情報を付加するための拡張。

これがないと、証明書を参照するシステム側で混乱が起きる場合がある。

BouncyCastleでは、以下のような手順で作成を行う。

// using Org.BouncyCastle.X509.Extension;

// using Org.BouncyCastle.Asn1;
// using Org.BouncyCastle.Crypto;
// AsymmetricCipherKeyPair keyPair;
// 公開鍵のみ必要で、秘密鍵は必要ない
var authkey = new AuthorityKeyIdentifierStructure(keyPair.Public);
var x509ext = new X509Extension(false,new DerOctetString(authkey));


証明書に拡張属性を付加する

作成する証明書に拡張属性を付与したい場合、以下のようにする。

X509Extension x509ext;

// x509extに値を設定
// 必須フラグとは、システム側で拡張を解釈できない場合、無効な証明書とみなすかどうか
// ほとんどのx509拡張はどちらかにすべきか仕様で決まっている
x509gen.AddExtension(X509Extensions.[拡張に対応するID],[必須フラグ],x509ext.GetParsedValue());


証明書署名要求(Certificate Signing Request=CSR)を作成する


準備

証明書を作成するにあたり、まず証明書が必要な側が、最低限以下の情報を通知する必要がある。


  • 公開鍵

  • X500識別名(Distinguished Name)

上記情報の伝達のための統一的なフォーマットがCSRであり、PKCS#10として定義されている。

事前準備として、以下のものを最低限CSR作成側で用意しておく。


  • 秘密鍵と公開鍵のペア

  • X500識別名

他にもx509の拡張属性が付加できる。中でもよく使われるのが、サブジェクト代替名、基本制約、使用用途だろう。

ただし、最終的な証明書への属性付加は認証局側で取捨選択できるので、ここで要求した内容がそのまま反映されるわけではないということに注意する。


サブジェクト代替名(SubjectAlternativeName)

X509証明書の仕様上、一つの証明書に結び付けられる名前は基本的には一つだけとなる。ただ、マルチドメインの証明書を一つの証明書で済ませたい場合など、複数の名前を一つの証明書に関連付けたい場合がある。

そのような時に使うのが、サブジェクト代替名という拡張属性である。

BouncyCastleにおいては、以下のようにしてX509Extensionインスタンスを作成する

// using Org.BouncyCastle.Asn1.X509

var altNames = new GeneralNames(new GeneralNames[]
{
// ドメイン名指定の場合
new GeneralName(GeneralNames.DnsName,"example.com"),
// IPアドレス指定の場合(文字列ではなくバイト配列で指定することに注意)
new GeneralName(GeneralNames.IPAddress,new DerOctetString(new byte[]{1,2,3,4})
});
var x509ext = new X509Extension(false,new DerOctetString(altNames));


使用用途(KeyUsage)

X509証明書には使用用途を限定するためのフラグが存在し、このフラグを利用すれば、ファイル署名のみに使用できる証明書等が作れる。

BouncyCastleにおいては、以下のようにしてX509Extensionインスタンスが作成できる。

// using Org.BouncyCastle.Asn1.X509

// ビットフラグを指定する
// KeyUsageのスタティッククラス変数として定義されている値を使用する
var usage = new KeyUsage(KeyUsage.DigitalSignature | KeyUsage.KeyCertSign);
var x509ext = new X509Extension(false,new DerOctetString(usage));

なお、KeyUsageでは一定の使い道しか指定できないことに注意。

より拡張された使い方を指定したい場合は、ExtendedKeyUsageクラスを使っていく


拡張された使用用途(ExtendedKeyUsage)

標準の使用用途ではカバーできない使い方の指定を行うために作られた。

BouncyCastleにおいては、以下のように作成する。

// using Org.BouncyCastle.Asn1.X509

var extusage = new ExtendedKeyUsage(KeyPurposeID.IdKPClientAuth,
KeyPurposeID.IdKPCodeSigning);
var x509ext = new X509Extension(false,new DerOctetString(extusage));


証明書署名要求の作成コード

以下のようにして作成する

// using Org.BouncyCastle.X509;

// using Org.BouncyCastle.Asn1;
// using Org.BouncyCastle.Asn1.X509;
// using Org.BouncyCastle.Pkcs;
// using Org.BouncyCastle.Crypto;
static void CreateCertificateRequest(string subjectDN,AsymmetricCipherKeyPair keyPair,bool isCA)
{
var name = new X509Name(subjectDN);
var signerFactory = new Asn1SignatureFactory(PkcsObjectIdentifiers.Sha256WithRsaEncryption.Id, keyPair.Private, new SecureRandom());
var extlst = new List<X509Extension>()
{
new X509Extension(true, new DerOctetString(new BasicConstraints(true).GetDerEncoded()))
};
var altNames = new GeneralNames(new GeneralName[]{
new GeneralName(GeneralName.DnsName,"*.example.com")
,
new GeneralName(GeneralName.IPAddress,new DerOctetString(new byte[]{1,2,3,4}))
});
var usage = new KeyUsage(KeyUsage.DigitalSignature | KeyUsage.KeyCertSign);
var extusage = new ExtendedKeyUsage(KeyPurposeID.IdKPClientAuth
, KeyPurposeID.IdKPCodeSigning);
var cons = new BasicConstraints(isCA);
var oids = new List<object>()
{
X509Extensions.SubjectAlternativeName,
X509Extensions.KeyUsage,
X509Extensions.ExtendedKeyUsage,
X509Extensions.BasicConstraints,
};
var values = new List<object>()
{
new X509Extension(false,new DerOctetString(altNames))
, new X509Extension(false,new DerOctetString(usage))
, new X509Extension(false,new DerOctetString(extusage))
, new X509Extension(true,new DerOctetString(cons))
};
var x509exts = new X509Extensions(oids, values);
var attr = new X509Attribute(PkcsObjectIdentifiers.Pkcs9AtExtensionRequest.Id, new DerSet(x509exts));
var req = new Pkcs10CertificationRequest(signerFactory, name, keyPair.Public, new DerSet(attr), keyPair.Private);
// バイト配列から復元したい場合
// Pkcs10CertificationRequest.GetInstance(bytes);
// 情報を取り出したい場合
// Pkcs10CertificationRequest req;
// [復元作業]
// var reqInfo = req.GetCertificationRequestInfo();
return req.GetDerEncoded();
}

出力されるバイト配列はDERエンコーディングなので、解読したい場合は openssl req -inform DER ... で解読できる


認証局の証明書を使用して、派生の証明書を作成する

本題である証明書の作成に入る。前提として、証明書署名要求のバイナリを認証局が受け取った状態から始める。


認証局側で用意するもの


  • 認証局の秘密鍵と公開鍵のペア


    • 要求側のペアとは異なるキーにしておくこと



  • 認証局の識別名(Distinguished Name)

  • シリアルナンバー


    • 桁数制限はないと思っていいので文字列形式で持った方がいい

    • 証明書ストアおよび派生先/元証明書のどの値とも重ならない値にすること




証明書作成コード例

基本的には自己証明書とあまり流れは変わらない。

// DERエンコーディングのx509証明書バイト配列を返す

static byte[] CreateSignedCert(
string issuerDN,
AsymmetricCipherKeyPair keyPair,
string subjectDN,
RsaKeyParameters subjectPublicKey,
DateTime notBefore,
DateTime notAfter,
bool isCA,
string serialNumber, int radix,
List<Tuple<AlternativeNameKind, string>> alternativeNames
)
{
var x509gen = new X509V3CertificateGenerator();
x509gen.SetSerialNumber(new BigInteger(serialNumber, radix));
x509gen.SetIssuerDN(new X509Name(issuerDN));
x509gen.SetSubjectDN(new X509Name(subjectDN));
x509gen.SetNotAfter(notAfter);
x509gen.SetNotBefore(notBefore);
x509gen.SetPublicKey(subjectKey);
var subkey = new SubjectKeyIdentifierStructure(subjectKey);
x509gen.AddExtension(X509Extensions.SubjectKeyIdentifier.Id
, false
, new X509Extension(false, new DerOctetString(subkey)).GetParsedValue());
var authkey = new AuthorityKeyIdentifierStructure(keyPair.Public);
x509gen.AddExtension(X509Extensions.AuthorityKeyIdentifier
, false
, new X509Extension(false,new DerOctetString(authkey)).GetParsedValue());
x509gen.AddExtension(X509Extensions.BasicConstraints, true, new BasicConstraints(isCA));
var signerFactory = new Asn1SignatureFactory(PkcsObjectIdentifiers.Sha256WithRsaEncryption.Id, keyPair.Private);
X509Extension ext = null;
GeneralNames genName = null;
if (alternativeNames != null && alternativeNames.Any())
{
genName = new GeneralNames(
alternativeNames.Select(x =>
{
switch (x.Item1)
{
case AlternativeNameKind.IP:
{
IPAddress ip;
if (IPAddress.TryParse(x.Item2, out ip))
{
return new GeneralName(GeneralName.IPAddress, new DerOctetString(ip.GetAddressBytes()));
}
else
{
return null;
}
}
case AlternativeNameKind.DnsName:
return new GeneralName(GeneralName.DnsName, new DerUtf8String(x.Item2));
case AlternativeNameKind.Rfc822:
return new GeneralName(GeneralName.Rfc822Name, new DerUtf8String(x.Item2));
default:
return new GeneralName(GeneralName.OtherName, new DerUtf8String(x.Item2));
}
}).Where(x => x != null).ToArray()
);
ext = new X509Extension(false, new DerOctetString(genName));
x509gen.AddExtension(X509Extensions.SubjectAlternativeName, false, ext.GetParsedValue());
}
var ret = x509gen.Generate(signerFactory);
return ret.GetEncoded();
}