Windows OpenSSL 無しで pem 形式のサーバー証明書と秘密鍵を IP アドレスベースで作成する (node.js版)
node.js で node-forge というパッケージを使用して pem 形式のサーバー証明書と秘密鍵を .pem 形式で作成する方法を紹介します。また既存の .pfx ファイルからサーバー証明書や秘密鍵を pem 形式で出力する方法も紹介します。
やりたいこと
以前 Python や PowerShell を使って自己署名のサーバー証明書 (所謂オレオレ証明書) や秘密鍵を pem 形式で作成する方法を紹介したのですが、JavaScript で開発している人こそ https をより使うのでは、ということで node.js 環境で node-forge というパッケージを使用して pem 形式のサーバー証明書や秘密鍵を作成してみたいと思います。また既存の .pfx ファイルからサーバー証明書や秘密鍵を .pem 形式で取り出してみます。なお、IP アドレスベースでの証明書の作り方で紹介していますが、ドメインの証明書の作り方も触れていますので参考にしてください。
環境
- OS
- Microsoft Windows 10 21H2
- node.js
- v20.10.0
- node-forge
- 1.3.1
準備
node-forge をインストールします。
npm install node-forgeES Modules 派なんですが、node-forge は ES Modules 対応されていないので、念のため package.json の "type" が設定されていないか "commonjs" となっているか確認してください。
If the nearest parent
package.jsonlacks a"type"field, or contains"type": "commonjs", .js files are treated as CommonJS.
- Modules: Packages | Node.js v21.7.1 Documentation
また以下の手順では適当なスクリプトファイル (index.js 等) を作成して node index.js で実行してもよいですし、node で対話型コマンドラインを立ち上げて逐次実行しても問題ありませんので好きなやり方で実行してください。
証明書と秘密鍵を新規作成して pem 形式で出力する方法
キーペアの作成
まずは秘密鍵と公開鍵のペアを作成します。
証明書や秘密鍵を PEM 形式で作成する場合は pki モジュールのみインポートすれば大丈夫です。また後程使用するので fs.writeFileSync もインポートしておきます。
const { pki } = require('node-forge');
const { writeFileSync } = require('node:fs');
const keys = pki.rsa.generateKeyPair(2048);ここでは RSA で作成していますが ED25519 にも対応しています。
const keys = pki.ed25519.generateKeyPair();証明書の初期設定
-
pki.createCertificate()で X.509 オブジェクトを作成します。const cert = pki.createCertificate(); cert.publicKey = keys.publicKey; -
cert.publicKeyに前項で作成したキーペアの公開鍵を設定します。cert.publicKey = keys.publicKey; -
cert.serialNumberに適当な数字文字列を設定します。cert.serialNumber = '01';シリアルナンバーは RFC では以下の通り発行する CA 内で一意にする必要がありますが、まあオレオレ証明書なので自分で識別できれば気にしなくて大丈夫でしょう。 デフォルトで
'00'が設定されているので、そもそも設定しなくてもよいかもしれません。The serial number MUST be a positive integer assigned by the CA to each certificate. It MUST be unique for each certificate issued by a given CA (i.e., the issuer name and serial number identify a unique certificate). CAs MUST force the serialNumber to be a non-negative integer.
- RFC 5280: Internet X.509 Public Key Infrastructure Certificate and Certificate Revocation List (CRL) Profile -
cert.validity.notBeforeとcert.validity.notAfterで証明書有効期限を設定します。ここでは一旦本日から 1 年間とします。cert.validity.notBefore = new Date(); cert.validity.notAfter = new Date(new Date().getFullYear(), new Date().getMonth(), new Date().getDate());
証明書コンテンツの設定
-
以下の通り Subject 設定用オブジェクトを用意します。
const subjects = [ { shortName:'CN',value:'xxx.xxx.xxx.xxx' }, { shortName:'C',value:'JP' }, { shortName:'ST',value:'Tokyo' }, ];ここでは
shortName: 'CN'のように X.500 の DN (Distinguished Name) ベースで設定していますが、name: 'commonName'のように設定することもできます。
なお、IP アドレスではなくドメイン名で証明書を作成する場合はCNのvalueは IP アドレスではなくドメイン名にしてください。 -
Subject を設定します。またオレオレなので Issuer にも同内容で設定します。
cert.setSubject(subjects); cert.setIssuer(subjects); -
次に Extension 設定用のオブジェクトを用意します。
const extensions = [{ name: 'subjectAltName', altNames: [{ type: 7, ip: 'xxx.xxx.xxx.xxx' }] }];IP アドレスではなくドメイン名で証明書を作成する場合は、以下のようにします。
altNames: [{ type: 7, value: 'xxx.domain.com' }]この
typeは RFC 5280 に則っています。SubjectAltName ::= GeneralNames GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName GeneralName ::= CHOICE { otherName [0] OtherName, rfc822Name [1] IA5String, dNSName [2] IA5String, x400Address [3] ORAddress, directoryName [4] Name, ediPartyName [5] EDIPartyName, uniformResourceIdentifier [6] IA5String, iPAddress [7] OCTET STRING, registeredID [8] OBJECT IDENTIFIER } -
Extension を設定します。
cert.setExtensions(extensions);
証明書に署名
最初に作成した秘密鍵でオレオレ署名します。
cert.sign(keys.privateKey);pem 形式で出力
- 証明書と秘密鍵を pem 形式に変換します。
const certPem = pki.certificateToPem(cert); const keyPem = pki.privateKeyToPem(key.privateKey); - それぞれファイルに書き出します。
writeFileSync('./cert.pem', certPem, { encoding: 'utf-8' }); writeFileSync('./key.pem', keyPem, { encoding: 'utf-8' });
おまけ PKCS #12 (PFX) 形式で出力
Node の https サーバーは PFX ファイルで証明書と秘密鍵を設定することができるので、pem ではなく pfx での出力方法も紹介します。
-
node-forgeからインポートするモジュールにpkcs12とasn1を追加します。const { pki, pkcs12, asn1 } = require('node-forge'); -
pkcs12.toPkcs12Asn1()メソッドに作成済の秘密鍵と証明書を渡して PKCS #12 の ASN.1 オブジェクトにします。const p12Asn1 = pkcs12.toPkcs12Asn1(keys.privateKey, cert, 'password', {algorithm: '3des'}); -
asn1.toDer()メソッドに PKCS #12 の ASN.1 オブジェクトを渡して DER 形式に変換します。const p12Der = asn1.toDer(p12Asn1); -
ファイルに書き出します。
writeFileSync('cert.pfx', p12Der.getBytes(), 'binary');
既存の pfx ファイルから PEM 形式の証明書と秘密鍵を取り出す
次に node-forge を使用して既存の pfx ファイルから pem 形式の証明書と秘密鍵を取り出す方法を紹介します。
-
必要なモジュールをインポートし、pfx ファイルを読み込みます。
const { asn1, pkcs12, pki } = require('node-forge'); const { readFileSync, writeFileSync } = require('node:fs'); const p12Der = readFileSync('cert.pfx', 'binary'); -
読み込んだ pfx ファイルはバイナリの DER 形式なので
asn1.fromDer()メソッドでnode-forgeの ASN.1 オブジェクトに変換します。const p12Asn1 = asn1.fromDer(p12Der); -
node-forgeの ASN.1 オブジェクトをnode-forgeの PKCS #12 オブジェクトに変換します。const p12 = pkcs12.pkcs12FromAsn1(p12Asn1);
証明書の取り出しと出力
-
証明書が格納されている SafeBag を取り出します。
const certBags = p12.getBags({ bagType: pki.oids.certBag });pki.oids.certBagは OID1.2.840.113549.1.12.10.1.3のエイリアスで PKCS #12 Certificate Bag です。 -
SafeBag 内の
node-forgeの証明書オブジェクトを pem に変換します。const certPem = pki.certificateToPem(certBags[pki.oids.certBag][0].cert); -
ファイルに出力します。
writeFileSync('cert.pem', certPem, { encoding: 'utf-8' });
秘密鍵の取り出しと出力
-
秘密鍵が格納されている SafeBag を取り出します。
const keyBags = p12.getBags({ bagType: pki.oids.pkcs8ShroudedKeyBag });pki.oids.pkcs8ShroudedKeyBagは OID1.2.840.113549.1.12.10.1.2のエイリアスで PKCS #12 PKCS #8-shrouded Key Bag です。通常こちらだと思うのですがもし見つからなければ以下のように PKCS #12 KeyBag (OID:1.2.840.113549.1.12.10.1.1) でも探してみてください。const keyBags = p12.getBags({ bagType: pki.oids.keyBag }); -
SafeBag 内の
node-forgeの秘密鍵オブジェクトを pem に変換します。const keyPem = pki.privateKeyToPem(keyBags['pki.oids.pkcs8ShroudedKeyBag'][0].key); -
ファイルに出力します。
writeFileSync('key.pem', keyPem, { encoding: 'utf-8' });
あとがき
node-forge を使って JavaScript だけでオレオレ証明書と秘密鍵を pem 形式で作成してみました。node-forge は機能豊富で他にもいろいろできそうです。また RFC や X.500 シリーズなどスタンダードな仕様に基づいた内部構造となっているので、各仕様がわかっていないと直観的には使いにくい反面、各仕様を見ればやっていることがわかったり、理解も進むので勉強にはなりました。