Windows OpenSSL 無しで pem 形式のサーバー証明書と秘密鍵を IP アドレスベースで作成する

Python cryptograpy パッケージを使用して .pfx 形式のサーバー証明書から秘密鍵と証明書を .pem 形式で取得する方法のメモです。

やりたいこと

本記事では途中から Python を使用しますが、すべて PowerShell だけで実行できる方法を紹介した 新しい記事 を書きました。よければご覧ください。本記事は今後 Python のみですべて実行できる記事に書き換える予定です。
またすべて node.js で実行できる方法を紹介した記事 もあります。こちらもよければご覧ください。

ブラウザの Web API の中には プロトコルが HTTPS でないと使用できないものが結構あります。例えば MediaDevices: getUserMedia() メソッド - Web API | MDN 等多数。外部公開していなくても LAN 内でアクセスしたいときでも HTTPS である必要があります。

そうした時にとりあえず自己署名証明書 (所謂オレオレ証明書) を用意しますが、Windows の標準ツールの場合、証明書を作成すると全て証明書ストアに配置されてしまいます。証明書ストアからエクスポートはできるのですが、エクスポート形式は .pfx (pkcs#12形式) のみです。.pfx ではなく .pem 形式の証明書と秘密鍵のペアでないと使えない Web サーバーもあるので、.pfx.pem に変換したいところですが、OpenSSL はインストールが結構面倒なので入れたくないと思い調べたところ Python の cryptography というパッケージを使用して変換することができたので、メモ兼ねてやり方を紹介します。

環境

OS
Microsoft Windows 11 22H2
PowerShell
5.1.22621.963
Python
3.11.3
pip
22.3.1

証明書作成から pem 変換までの手順

証明書作成

PowerShell を開いて以下のコマンドで秘密鍵と証明書を作成します。

$cert=New-SelfSignedCertificate -Subject "C=JP,ST=Tokyo,CN=xxx.xxx.xxx.xxx" -TextExtension @("2.5.29.17={text}IPAddress=xxx.xxx.xxx.xxx") -KeyAlgorithm RSA -KeyLength 2048 -KeyExportPolicy Exportable -NotAfter (Get-Date).AddYears(1) -CertStoreLocation "Cert:\CurrentUser\My"

各パラメータの内容は以下の通りです。詳細は New-SelfSignedCertificate (pki) | Microsoft Learn をご覧ください。

  • Subject: 証明書のサブジェクトです。C は Country で国コード、ST は State で日本の場合は都道府県、CN は Common Name でサーバーの IP アドレスを指定します。(ちなみにドメインで証明書発行する場合はここをドメインにします)
  • TextExtension: 証明書の拡張項目を OID で指定して設定します。OID=2.5.29.17 は Subject Alternative Name です。(ちなみにドメインで証明書発行する場合は代わりに -DnsName オプションでドメイン名を指定します)
  • KeyAlgorithm: 鍵の作成アルゴリズムです。
  • KeyLength: 鍵の長さです。
  • KeyExportPolicy: 秘密鍵のエクスポートポリシーです。エクスポート可能にしています。
  • NotAfter: 証明書の有効期限です。1年後を期限としています。
  • CertStoreLocation: 証明書を作成する証明書ストア内の場所です。ここでは「現在のユーザー」の「個人」に作成しています。(なお「現在のユーザー」でも「ローカルコンピューター」でも「個人」にしか作成できません)

なお、ここでサーバーの IP アドレスをサブジェクトの Common Name と Subject Alternative Name の双方にセットしていますが、RFC 2818: HTTP Over TLS では以下の通り記載されており、Common Name は廃止しており、Subject Alternative Name の DNS の値を使用すべきとされていますが、一部古いブラウザでは Common Name を見てる可能性もあるので念のため設定しています。Chrome は結構前から Common Name は見ておらず、Subject Alternative Name の DNS (または IPAdress) が設定されていないと証明書エラーになります。

If a subjectAltName extension of type dNSName is present, that MUST be used as the identity. Otherwise, the (most specific) Common Name field in the Subject field of the certificate MUST be used. Although the use of the Common Name is existing practice, it is deprecated and Certification Authorities are encouraged to use the dNSName instead. - RFC 2818: HTTP Over TLS

証明書のエクスポート

次に作成した証明書を .pfx でエクスポートします。ここでは PowerShell のコマンドベースでエクスポートしますが certmgr の GUI ベースでエクスポートしても問題ありません。

  1. PowerShell で以下のコマンドでパスワードを作成します。

    $pw = ConvertTo-SecureString -String 'mypassword' -Force -AsPlainText
  2. PowerShell で以下のコマンドで証明書を .pfx でエクスポートします。$cert は証明書作成で作成した証明書、$pw は作成したパスワードです。Password オプションは SecureString 型となっているので、直接文字列で指定すると InvalidArgument エラーになるので注意してください。

    Export-PfxCertificate -Cert $cert -FilePath ./cert.pfx -Password $pw

.pfx.pem に変換

ではいよいよ .pfx.pem に変換していきます。

  1. cryptograpy パッケージをインストールします。
    pip install cryptograpy

 

  1. 次のようなコードを書いて、pfx2pem.py として保存します。pfxファイルのパスやパスワードは適宜置き換えてください。
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.serialization import pkcs12
from cryptography.hazmat.backends import default_backend

pfx_file = './cert.pfx'
password = b'mypassword'

with open(pfx_file, 'rb') as file:
  pfx_data = file.read()

private_key, certificate, _ = pkcs12.load_key_and_certificates(
  pfx_data, 
  password, 
  default_backend()
  )

private_key_pem = private_key.private_bytes(
  encoding=serialization.Encoding.PEM,
  format=serialization.PrivateFormat.PKCS8,
  encryption_algorithm=serialization.NoEncryption()
  )

certificate_pem = certificate.public_bytes(
  encoding=serialization.Encoding.PEM
  )

with open('./cert.pem', 'w', encoding='utf-8') as certificate_file:
  certificate_file.write(certificate_pem.decode('utf-8'))

with open('./privkey.pem', 'w', encoding='utf-8') as private_key_file:
  private_key_file.write(private_key_pem.decode('utf-8'))
  1. pfx2pem.py を実行します。

    python pfx2pem.py
  2. カレントディレクトリに cert.pemprivkey.pem が出力されますので、適宜 Web サーバー等で利用してみてください。なお、接続するクライアント側の「信頼されたルート証明書」にエクスポートした .pfx をインポートしないと証明書エラーになるので注意してください。

あとがき

これまで自己署名証明書作る際に Mac や Linux で OpenSSL で作成して持ってきたり、WSL で Linux 動かして作成したりしてたのですが、Windows で Python のパッケージ一つで .pem 形式で作成できたのでものすごく楽になりました。PowerShell のサードパーティーモジュールである Convert-PfxToPem というモジュールを使えば PowerShell だけで完結できそうなのですがうまく動かなくて、ソース眺めたりしてたのですが Win32 API がいっぱいでてきて心折れたので諦めました…。