本文へスキップ
ecdsa.com

PKCS#1 と PKCS#8|BEGIN RSA PRIVATE KEY か PRIVATE KEY か

どちらも同じ RSA の数値を保持します。PKCS#8 はアルゴリズム識別子とオプションの暗号化を加えた形式で、そのため最近のライブラリはどれも PKCS#8 を優先し、それ以外を受け付けないものもあります。

「サポートされていない鍵形式」というエラーの多くは、PKCS#1 と PKCS#8 の違いが原因です。たいていは、何年も前に openssl genrsa で生成した鍵を最近のライブラリで読み込もうとしたときに起こります。PKCS#1 のファイル(-----BEGIN RSA PRIVATE KEY-----)には RSA 鍵の構造しか入っていません。一方、PKCS#8 のファイル(-----BEGIN PRIVATE KEY-----)は、任意のアルゴリズムの秘密鍵をアルゴリズム識別子とともに包み、その暗号化形式(-----BEGIN ENCRYPTED PRIVATE KEY-----)は標準的なパスワード保護を加えます。

比較表

PKCS#1 RSAPrivateKeyPKCS#8 PrivateKeyInfo / OneAsymmetricKey
仕様RFC 8017 付録 A.1.2RFC 5208、RFC 5958
PEM ラベルRSA PRIVATE KEYPRIVATE KEY。暗号化時は ENCRYPTED PRIVATE KEY
アルゴリズムRSA のみ任意:RSA、EC、Ed25519、X25519、ML-DSA、ML-KEM …
アルゴリズム識別子なし(ラベルで暗示)rsaEncryption、id-ecPublicKey、1.3.101.112 …
暗号化OpenSSL 独自の PEM ヘッダー(Proc-Type、DEK-Info)、弱い KDFRFC 8018 に基づく標準の PBES2(PBKDF2 と AES)
OpenSSL 3 の出力-traditional 指定時のみgenpkey、pkey、rsa のデフォルト
Java の PKCS8EncodedKeySpec非対応対応
Gox509.ParsePKCS1PrivateKeyx509.ParsePKCS8PrivateKey
Node.js の createPrivateKey対応(PEM ラベルで判別)対応
WebCrypto の importKey非対応pkcs8 形式

公開鍵でこれらに対応するのは PKCS#1 の RSAPublicKey(BEGIN RSA PUBLIC KEY)と SubjectPublicKeyInfo(BEGIN PUBLIC KEY)で、ほかのあらゆるソフトウェアが想定するのは後者です。

それぞれの中身

PKCS#1 の RSA 秘密鍵は、バージョン、モジュラス n、公開指数 e、秘密指数 d、素数 p と q、CRT の値 dP、dQ、qInv を並べた DER の SEQUENCE です。構造自体はアルゴリズムについて何も示さないため、PEM ラベルがそれを示さなければなりません。楕円曲線の鍵には PKCS#1 の形式はありません(OpenSSL でこれに相当する BEGIN EC PRIVATE KEY 形式は、SEC 1、RFC 5915 に由来します)。

PKCS#8 は、アルゴリズム固有の鍵を PrivateKeyInfo で包みます。中身はバージョン、AlgorithmIdentifier、内側の鍵を格納した OCTET STRING で、RSA の場合、この内側の鍵は PKCS#1 の構造そのものです。RFC 5958(バージョン 2、OneAsymmetricKey)では公開鍵も格納できます。EncryptedPrivateKeyInfo は暗号化アルゴリズムと暗号文を加えたもので、OpenSSL 3.6 で暗号化を指定すると、PBKDF2-HMAC-SHA256 と AES-256-CBC を使う PBES2 が使われます。

変換

次のスクリプトは OpenSSL 3.6.5 で実行したもので、デフォルトの形式と、情報を失わない往復変換を示します。

OpenSSL 3.6:PKCS#8、PKCS#1、暗号化された PKCS#8
#!/bin/sh
dir=$(mktemp -d)
cd "$dir" || exit 1
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out pkcs8.pem 2>/dev/null
openssl pkey -in pkcs8.pem -traditional -out pkcs1.pem
openssl pkey -in pkcs8.pem -aes256 -passout pass:demo -out enc.pem
head -1 pkcs8.pem; head -1 pkcs1.pem; head -1 enc.pem
openssl pkcs8 -topk8 -nocrypt -in pkcs1.pem -out back.pem && head -1 back.pem
cmp -s pkcs8.pem back.pem && echo "round trip PKCS#1 -> PKCS#8: identical"
cd / && rm -rf "$dir"
出力
-----BEGIN PRIVATE KEY-----
-----BEGIN RSA PRIVATE KEY-----
-----BEGIN ENCRYPTED PRIVATE KEY-----
-----BEGIN PRIVATE KEY-----
round trip PKCS#1 -> PKCS#8: identical

同じコマンドで、SEC 1 の EC 鍵(BEGIN EC PRIVATE KEY)も PKCS#8 に変換できます。OpenSSH の秘密鍵(BEGIN OPENSSH PRIVATE KEY)は第 3 の形式で、ssh-keygen -p -m PKCS8 -f key を使うと、そのファイルを PKCS#8 としてその場で書き換えられます。

遭遇するエラー

最も厳格なのは Java です。PKCS8EncodedKeySpec は PKCS#8 しか受け付けず、エラーの文言はリリースによって変わっています。次のプログラムは各種類の PEM ファイルを 1 つずつ読み込み、その後のコマンドは、公式の Eclipse Temurin イメージを使ってこのプログラムを Java 17、21、25 で実行します。

LoadKey.java:PKCS8EncodedKeySpec で PEM の秘密鍵を読み込む
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyFactory;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;

public class LoadKey {
  public static void main(String[] args) throws Exception {
    System.out.println("== Java " + Runtime.version());
    for (String file : args) {
      String pem = Files.readString(Path.of(file));
      byte[] der = Base64.getMimeDecoder().decode(pem.replaceAll("-----[A-Z ]+-----", ""));
      try {
        KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(der));
        System.out.println(file + ": loaded");
      } catch (Exception e) {
        System.out.println(file + ": " + e);
        for (Throwable t = e.getCause(); t != null; t = t.getCause()) System.out.println("  Caused by: " + t);
      }
    }
  }
}
PKCS#8 と PKCS#1 の鍵を用意して Java 17、21、25 で実行
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out pkcs8.pem 2>/dev/null
openssl pkey -in pkcs8.pem -traditional -out pkcs1.pem
for v in 17.0.20.1_1 21.0.12.1_1 25.0.4.1_1; do
  docker run --rm -v "$PWD":/work:ro -w /work "eclipse-temurin:$v-jdk-noble" java LoadKey.java pkcs8.pem pkcs1.pem
done
出力(2026年10月20日)
== Java 17.0.20.1+1
pkcs8.pem: loaded
pkcs1.pem: java.security.spec.InvalidKeySpecException: java.security.InvalidKeyException: IOException : algid parse error, not a sequence
  Caused by: java.security.InvalidKeyException: IOException : algid parse error, not a sequence
== Java 21.0.12.1+1-LTS
pkcs8.pem: loaded
pkcs1.pem: java.security.spec.InvalidKeySpecException: java.security.InvalidKeyException: Unable to decode key
  Caused by: java.security.InvalidKeyException: Unable to decode key
  Caused by: java.io.IOException: algid parse error, not a sequence
== Java 25.0.4.1+1-LTS
pkcs8.pem: loaded
pkcs1.pem: java.security.spec.InvalidKeySpecException: java.security.InvalidKeyException: Unable to decode key
  Caused by: java.security.InvalidKeyException: Unable to decode key
  Caused by: java.io.IOException: algid parse error, not a sequence

Web 上の古い回答は Java 17 の文言を引用しています。Java 21 と 25 では最上位のメッセージは Unable to decode key で、「algid parse error, not a sequence」は原因の例外にしか残っていません。ログは両方の文言で検索してください。

ほかのランタイムの結果は次のとおりです(同じ日に、OpenSSL 3.6.5 で生成した鍵で再現しました)。

  • Go:x509.ParsePKCS8PrivateKey は x509: failed to parse private key (use ParsePKCS1PrivateKey instead for this key format) を返します(Go 1.25)。何が起きたのかがそのまま分かるメッセージです。
  • Node.js 26(createPrivateKey、OpenSSL 3.6.5):type: "pkcs8" と指定した PKCS#1 の DER は error:068000A8:asn1 encoding routines::wrong tag(コード ERR_OSSL_ASN1_WRONG_TAG)というエラーになります。BEGIN PRIVATE KEY ラベルの下に PKCS#1 の本体を入れると error:1E08010C:DECODER routines::unsupported になり、同じ PKCS#1 のバイト列を spki として createPublicKey に渡すと Failed to read asymmetric key になります。暗号化された鍵をパスフレーズなしで読み込むと error:07880109:common libcrypto routines::interrupted or cancelled になります。
  • 旧来のアルゴリズムで暗号化された鍵:DES で暗号化した従来形式の PEM 鍵や、MD5 と DES による PBES1 で保護した PKCS#8 の鍵は、OpenSSL 3 ではレガシープロバイダーを使わないと読み込めません。レガシープロバイダーがないと、openssl pkey は error:0308010C:digital envelope routines:inner_evp_generic_fetch:unsupported に続けて、取得できなかったアルゴリズム(DES-CBC、PBES1 の場合は PBKDF1)を出力し、Node.js は error:0308010C:digital envelope routines::unsupported(ERR_OSSL_EVP_UNSUPPORTED)を報告します。
  • Python(cryptography 46):load_pem_private_key は PKCS#1 も PKCS#8 も同じように読み込み、暗号化された鍵をパスワードなしで読み込むと TypeError: Password was not given but private key is encrypted を送出します。
  • WebCrypto:PKCS#1 のバイト列を渡した importKey("pkcs8", …) は DataError で失敗します。Node.js 26 ではメッセージが Invalid keyData になり、ブラウザーはそれぞれ独自の文言を使います。

エラーカタログにはそれぞれのエラーのページがあり、このページの下にリンクを掲載しています。

公開鍵にも同じ区別がある

公開鍵にも同じ 2 つの形があります。PKCS#1 の RSAPublicKey(BEGIN RSA PUBLIC KEY)はモジュラスと指数だけを保持します。SubjectPublicKeyInfo(BEGIN PUBLIC KEY、RFC 5280)は鍵をアルゴリズム識別子とともに包み、あらゆるアルゴリズムに対応します。証明書、JWK 変換ツール、Java の X509EncodedKeySpec、Go の ParsePKIXPublicKey、WebCrypto の spki インポートが想定するのもこちらです。両者の変換には openssl rsa -pubin -in key.pem -RSAPublicKey_out と openssl pkey -pubin -in key.pem -pubout を使います。混乱はどちらの方向にも起こります。SPKI を書き出すツールがファイルに id_rsa.pub のような名前を付けることは多く、OpenSSH 独自の 1 行形式の公開鍵(ssh-rsa AAAA…)はさらに別の形式で、ssh-keygen -e -m PKCS8 で変換できます。

どちらを選ぶべきか

  • 新しい鍵(アルゴリズムを問わず):PKCS#8 を使います(ディスク上では暗号化し、それ自体が暗号化を行うシークレットマネージャーの中では暗号化しません)。
  • Java、Go、.NET、WebCrypto、クラウド KMS へのインポート:PKCS#8(DER または PEM)を使います。
  • RSA PRIVATE KEY しか読めない古いソフトウェア(一部のアプライアンス、古い Ruby や PHP のコード、旧来の SSH ツール):-traditional で PKCS#1 を出力します。できれば、受け渡しの時点に限ってください。
  • Windows、macOS、Java のキーストア向けの鍵と証明書:PKCS#12 を使います。PKCS#12 は内部に PKCS#8 の鍵を含みます。
  • SSH 鍵:ssh 自体には OpenSSH 形式を、別のツールで同じ鍵が必要な場合は PKCS#8 を使います。

移行時の注意点

  1. PKCS#1 と SEC 1 のファイルを探す:設定リポジトリとシークレットストア全体に対して grep -l "BEGIN RSA PRIVATE KEY\|BEGIN EC PRIVATE KEY" を実行します。
  2. 一度だけ変換する:openssl pkcs8 -topk8 -nocrypt(暗号化する場合は -v2 aes-256-cbc)で変換し、鍵を読み込む側を更新します。鍵の中身も、対応する証明書も変わりません。
  3. 暗号化された古い鍵を暗号化し直す:従来形式の PEM の暗号化(Proc-Type と DEK-Info ヘッダー)は、MD5 を 1 回反復するだけで鍵を導出します。また、DES や RC2 で暗号化された鍵には、どちらの形式でも OpenSSL のレガシープロバイダーが必要です。PBES2 と AES-256 で暗号化し直してください。
  4. コードを汎用パーサーに切り替える(ParsePKCS8PrivateKey、load_pem_private_key、createPrivateKey):これで、Ed25519 や ML-DSA を含む新しいアルゴリズムが、コードを変更せずに動作します。
  5. 検証する:変換した鍵で署名し、その署名を既存の証明書で検証します。鍵生成ツールと ASN.1 エクスプローラーを使えば、何も送信せずに構造を確認できます。

参考資料

すべての比較