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

PowerShell だけでサーバー証明書と秘密鍵を .pem 形式で作成する方法を紹介します。また既存の .pfx ファイルや Windows 証明書ストアの証明書を .pem 形式でエクスポートする方法も紹介します。

やりたいこと

以前 こちらの記事 で Windows で OpenSSL なしで .pem 形式の証明書や秘密鍵の作成方法を紹介したのですが、PowerShell と Python の両方を使う形になっていて中途半端だなと思っていたのと、その後の調査で PowerShell だけで全部やれることがわかったので、改めて本記事で PowerShell だけで一気通貫に .pem 形式の証明書や秘密鍵を作成する方法を紹介したいと思います。また元記事は逆に全部 Python で作成できるように今後記事の内容を変更しようと思います。

環境

OS
Microsoft Windows 10 21H2
PowerShell
7.3.6

PowerShell で証明書作成から 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

証明書を pem 形式でエクスポート

作成した証明書を .pem 形式に変換していきます。なお、ここで前項で作成した証明書ではなく、既存の .pfx ファイルを使用したい場合は .pfx を一度証明書ストアに取り込んでください。.pfx ファイルを開くと「証明書のインポートウィザード」が開きますので画面に沿ってインポートしてください。

また、既存の証明書ストアにある証明書を使用したい場合も同様ですが、以下コマンドでエクスポートしたい証明書の Thumbprint を確認してください。

Get-ChildItem Cert:\CurrentUser\My

ここでは「現在のユーザー」の「個人」内の証明書をリストしていますが、「ローカルコンピューター」の場合は CurrentUserLocalMachine にしてください。

確認した Thumbprint の値を使用して以下のように証明書をオブジェクトとして $cert に格納してください。念のためですが、前項で作成した証明書を使用する場合は既に $cert に証明書オブジェクトが格納されていますのでこの作業は不要です。

$cert = Get-ChildItem Cert:\CurrentUser\My |
  where Thumbprint -eq '上記で確認した Thumbprint の値'

さて、前置きが長くなりましたがここからは前項で作成した証明書を使用するケースも、既存の証明書を使用するケースも同様に進めていきます。

  1. 以下のコマンドで証明書を Base64 エンコードされた文字列に変換できます。

    $CertBase64 = [System.Convert]::ToBase64String($cert.RawData)

    この [System.Convert]::ToBase64String() メソッドは第 2 引数で改行の挿入を指定できるのですが、その単位が何故か 76 文字ごとという…。

    ToBase64String(Byte[], Base64FormattingOptions)
    8 ビット符号なし整数の配列を、Base64 の数字でエンコードされた等価の文字列形式に変換します。 戻り値に改行を挿入するかどうかを指定できます。

    public static string ToBase64String (byte[] inArray, Base64FormattingOptions options);

    パラメーター inArray Byte[]
    8 ビット符号なし整数の配列。

    options Base64FormattingOptions
    76 文字ごとに改行を挿入する場合は InsertLineBreaks。改行を挿入しない場合は None

    一方、pem ファイルの形式は 1 行 64 文字とすることが規定されています。

    Generators MUST wrap the base64-encoded lines so that each line consists of exactly 64 characters except for the final line, which will encode the remainder of the data (within the 64-character line boundary), and they MUST NOT emit extraneous whitespace.

  2. 仕方ないので 64 文字で改行しつつ、ついでに開始・終了行とラベル付けをする関数を用意します。
    開始・終了行やラベルについても RFC 7468 で定義されています。

    Textual encoding begins with a line comprising “—–BEGIN “, a label, and “—–”, and ends with a line comprising “—–END “, a label, and “—–”.
    The type of data encoded is labeled depending on the type label in the “—–BEGIN " line (pre-encapsulation boundary). For example, the line may be “—–BEGIN CERTIFICATE—–” to indicate that the content is a PKIX certificate (see further below). Generators MUST put the same label on the “—–END " line (post-encapsulation boundary) as the corresponding “—–BEGIN " line.

    function Format-Pem($StrBase64, $Label) {
      $Pem = "-----BEGIN " + $Label + "-----`r`n"
      for ($i = 0; $i -lt [Math]::Ceiling($StrBase64.Length / 64); $i ++) {
        $StartIndex = $i * 64
        $StrCount = 64
        if ($StartIndex + $StrCount -gt $StrBase64.Length) {
          $StrCount = $StrBase64.Length - $StartIndex
        }
        $Pem += ($StrBase64.Substring($StartIndex, $StrCount) + "`r`n")
      }
      $Pem += ("-----END " + $Label + "-----")
      return($Pem)
    }

    ちょっと長いですが、対話形式で PowerShell を使っていても、全部貼り付ければそのセッション内で定義した関数を使用できます。

  3. 作成した Format-Pem 関数を使って 64 文字ごとに改行しつつ、開始・終了行も追加します。

    $PemCert = Format-Pem $CertBase64 'CERTIFICATE'
  4. 最後にカレントディレクトリの cert.pem ファイルに書き出します。

    $PemCert | Out-File -FilePath cert.pem -Encoding Ascii

秘密鍵を pem 形式でエクスポート

  1. RSACertificateExtensions.GetRSAPrivateKey() メソッドに証明書オブジェクト $cert を渡して RSACng オブジェクト を取得します

    $RSACng = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert)
  2. CngKey.Export() メソッドで秘密鍵を PKCS #8 形式のバイト列として取得します。

    $KeyBytes = $RSACng.Key.Export([System.Security.Cryptography.CngKeyBlobFormat]::Pkcs8PrivateBlob)
  3. 取得したバイト列を Base64 エンコードされた文字列に変換します。

    $KeyBase64 = [System.Convert]::ToBase64String($KeyBytes)
  4. 証明書エクスポートの際に作成した Format-Pem 関数を使用して 64 文字ごとに改行しつつ、開始・終了行も追加します。

    $PemKey = Format-Pem $KeyBase64 'PRIVATE KEY'
  5. 最後にカレントディレクトリの key.pem ファイルに書き出します。

    $PemKey | Out-File -FilePath key.pem -Encoding Ascii

これで PowerShell だけで証明書と秘密鍵を .pem 形式で作成することができました。

あとがき

これまで自己署名証明書作る際に Mac や Linux で OpenSSL で作成して持ってきたり、WSL で Linux 動かして作成したりしてたのですが、Windows で PowerShell だけで.pem 形式で作成できたのでものすごく楽になりました。他にも Python だけで作成したり、node.js だけで作成したりもできそうなので今後紹介したいと思います。