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-forge
ES Modules 派なんですが、node-forge
は ES Modules 対応されていないので、念のため package.json
の "type"
が設定されていないか "commonjs"
となっているか確認してください。
If the nearest parent
package.json
lacks 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 シリーズなどスタンダードな仕様に基づいた内部構造となっているので、各仕様がわかっていないと直観的には使いにくい反面、各仕様を見ればやっていることがわかったり、理解も進むので勉強にはなりました。