EternalWindows
自己署名入り証明書 / 証明書の形式

証明書は、広く信頼されるCAによって発行されたものを 利用するのが1つの原則といえます。 これは、証明書に含まれるCAの署名が、多くのコンピュータ上で認識され、 それによって証明書に書かれている所有者情報が信頼できることになるからです。 証明書を自作するとは、証明書に作成者自らが署名を行うことを意味しますが、 その作成者は信頼できるCAとして他のコンピュータ上で認識されていないため、 証明書の内容は信頼することができません。 つまり、自作の証明書である自己署名入り証明書は、 作成者の身元を証明する手段として成立しません。

それでは、自己署名入り証明書には、どのような使い道があるのでしょうか。 1つは、署名アプリケーションの開発です。 署名に使う証明書には関連する秘密鍵が必要になりますが、 システムに既定でインストールされている証明書はCAから配布されたものであり、 秘密鍵はCAによって維持されています。 このため、既定で存在する証明書は署名に利用することはできません。 しかし、自己署名入り証明書の場合は、ローカルコンピュータ上で鍵ペアを作成するため、 証明書から関連する秘密鍵を参照することができます。 したがって、署名に利用することができます。 ただし、その署名したファイルやデータを公開する場合は、 自己署名入り証明書ではなく、信頼されたCAから発行されたものを使用する必要があります。 ちなみに、CryptoAPIは指定された証明書が自己署名であるかなどは問いません。

自己署名入り証明書のもう1つの利用は、独自のルートCAを構築することです。 インターネットのようなグローバルでない社内ネットワークの場合、 特定のコンピュータに自己署名入り証明書をインストールすることができますから、 そのコンピュータをルートCAとして見立てることができます。 社内ネットワーク上の各コンピュータは、ルートCAのコンピュータに対して証明書の申請をし、 ルートCAは自身の秘密鍵を使用して証明書を発行します。 取得した証明書をスマートカードなどに格納することにより、 社員証としての利用などが考えられます。

証明書の形式は、 ITU-Tによって勧告されたX.509という規格にて決定されています。 X.509は、証明書に含まれる情報やフォーマットを定義し、 現在ではバージョン3であるX.509v3の証明書が主に利用されています。 CryptoAPIのCERT_INFO構造体は、このX.509v3の形式が含んでいるデータを 開発者用に分かりやすく構成しています。

typedef struct _CERT_INFO {
  DWORD                       dwVersion;
  CRYPT_INTEGER_BLOB          SerialNumber;
  CRYPT_ALGORITHM_IDENTIFIER  SignatureAlgorithm;
  CERT_NAME_BLOB              Issuer;
  FILETIME                    NotBefore;
  FILETIME                    NotAfter;
  CERT_NAME_BLOB              Subject;
  CERT_PUBLIC_KEY_INFO        SubjectPublicKeyInfo;
  CRYPT_BIT_BLOB              IssuerUniqueId;
  CRYPT_BIT_BLOB              SubjectUniqueId;
  DWORD                       cExtension;
  PCERT_EXTENSION             rgExtension;
} CERT_INFO, *PCERT_INFO;

dwVersionからSubjectPublicKeyInfoまでは、バージョン1で規格されているデータを表します。 IssuerUniqueIdとSubjectUniqueIdは、バージョン2で追加されたデータを表しますが、 多くの場合はこれらを使用せず、0で初期化することになるでしょう。 そして、cExtensionとrgExtensionはバージョン3で追加されたデータを表し、 このデータは証明書の拡張情報という位置づけになっています。 拡張情報については後の節で説明しますが、 たとえば、証明書に「サーバー認証」というような目的を設定することができます。 今回は、証明書のバージョンを表すdwVersionの初期化について説明します。

CERT_INFO certInfo;

certInfo.dwVersion = CERT_V3;

dwVersionには、証明書のバージョンを表す定数を指定します。 CERT_V3は、現在の主流であるバージョン3を表すため、これを指定します。 仮に証明書がバージョン1のデータのみしか必要としない場合でも、 証明書のバージョンは3にすることができます。 最終的に一通りのメンバを初期化し終えた場合、 CERT_INFO構造体は暗号化されたバイトデータに変換することになります。 このバイトデータは、CAまた自己の署名を含んでおり、 X.509形式の証明書として識別可能なデータです。 つまり、このバイトデータをファイルに保存すれば、 証明書を作成できることになります。 なお、多くのCryptoAPIは、このバイトデータを包括したCERT_CONTEXT構造体で 証明書を表現しています。

typedef struct _CERT_CONTEXT {
  DWORD         dwCertEncodingType;
  BYTE*         pbCertEncoded;
  DWORD         cbCertEncoded;
  PCERT_INFO    pCertInfo;
  HCERTSTORE    hCertStore;
} CERT_CONTEXT,  *PCERT_CONTEXT;
typedef const CERT_CONTEXT *PCCERT_CONTEXT;

証明書を返す関数が、バイトデータではなくCERT_CONTEXT構造体を返す理由は、 アプリケーションの利便性を考慮してのことだと思われます。 CERT_CONTEXT構造体を返す関数は、その内部でpbCertEncoded(バイトデータ)の一部を 適切にデコードし、それをpCertInfoのメンバに設定してくれるため、 アプリケーションはpCertInfoから必要な情報に直ぐにアクセスすることができます。

RFC3280とASN.1について

インターネット上で利用される技術を標準化するIETFという団体は、 その各技術についての仕様をRFCという文書にて公開しています。 この文書の通し番号3280には、X.509証明書とCRL(証明書失効リスト)のプロファイルに ついて記述されており、証明書に含まれる各種情報の役割を理解するためには、 この文書に目を通すことは必須といえます。 X.509証明書の構造は、ASN.1(Abstract Syntax Notation One)という言語で記述され、 プラットフォームに依存しない抽象的な型を利用しています。 次に、ASN.1による定義の一例を示します。

TBSCertificate :: = SEQUENCE {
	version         [0]  EXPLICIT Version DEFAULT v1,
	serialNumber         CertificateSerialNumber,
	signature            AlgorithmIdentifier,
	issuer               Name,
	validity             Validity,
	subject              Name,
	subjectPublicKeyInfo SubjectPublicKeyInfo,
	issuerUniqueID  [1]  IMPLICIT UniqueIdentifer OPTIONAL,
	subjectUniqueID [2]  IMPLICIT UniqueIdentifer OPTIONAL,
	extensions      [3]  EXPLICIT Extensions OPTIONAL
}

この文は、TBSCertificateという型(正確にはASN.1 オブジェクト)を定義しています。 SEQUENCEというのは、複数の型を定義する際に指定する型で、 上記の文はC言語で言うところの、struct TBSCertificateを記述しているのと同じ意味を持ちます。 括弧内に記述されているのは、TBSCertificateのメンバであり、 左側が変数名(正確には識別名)で右側が型になります。 ASN.1では、型を独自に定義することが許可されていますが、 最終的にはそれはプリミティブと呼ばれる原型が基になっています。 これはいわば、DWORDがunsigned longが基にしているのと同じ要領です。 たとえば、CertificateSerialNumberは次のように定義されています。

CertificateSerialNumber ::= INTEGER

この文は、INTEGER型の情報を持つCertificateSerialNumberという型を定義しています。 INTEGERというのは整数型の原型であり、 このようなプリミティブは他に、BOOLEANやBIT STRINGなどがあります。

ASN.1はあくまで構造を定義するだけですから、 最終的にはアプリケーションはその構造に従ったインスタンスを作成しなければなりません。 このインスタンスの作成には、DER(Distinguished Encoding Rules)というエンコード方式を使用し、 このような変換の規則を設けることで、どのようなプラットフォームでも 作成されたインスタンスを扱うことができるようになります。 DERエンコードの形式は、タグ番号、データの長さ、実際のデータ、というようになっており、 デコード側はタグの値を確認することで、どのようなデータが格納されているかを 特定することになります。 たとえば、"abc"という文字列をDERエンコードすると、次のようになると思われます。

0x16 0x03 0x61 0x62 0x63

ASN.1におけるデータのサイズの最小単位は1バイト(オクテット)であり、 この例では全部の5バイトのデータが並んでいます。 最初の値はタグ番号であり、0x16は文字列を表す型を意味します。 2バイト目はデータの長さが格納され、ここでは0x03となっています。 これは、指定したデータが"abc"が3バイトであったため、その通りの値が格納されています。 そして、データの長さの後は実際のデータが格納されるということで、 "abc"の各文字のASCIIコード表における値が格納されています。 なお、タグ番号によっては、データが変形されて格納されることもあります。

実際のところ、アプリケーションがASN.1の各種タグ番号の変換形式を理解し、 自らDERエンコードやデコードのコードを記述することはまずないといえるでしょう。 エンコーディングタイプとしてX509_ASN_ENCODINGを指定し、 エンコードの際にはCertStrToNameやCryptEncodeObject、 デコードの際にはCertNameToStrやCryptDecodeObjectを呼び出せば、 それで作業は終了することになります。 知っておくべきことは、そのような作業がどのような場合において必要になるかであり、 たとえば、CERT_INFO構造体のIssuerメンバとSubjectメンバ、 さらに拡張情報などはDERエンコードされたままとなっているため、 これらの情報を取得したい場合はデコード用の関数を呼び出すことになります。



戻る