Выбор между PKCS#1 и PKCS#8 стоит за многими ошибками «unsupported key format»: обычно с ними сталкиваются, когда ключ, сгенерированный много лет назад командой openssl genrsa, попадает в современную библиотеку. Файл PKCS#1 (-----BEGIN RSA PRIVATE KEY-----) содержит только структуру ключа RSA; файл PKCS#8 (-----BEGIN PRIVATE KEY-----) оборачивает закрытый ключ любого алгоритма вместе с идентификатором алгоритма, а его зашифрованная форма (-----BEGIN ENCRYPTED PRIVATE KEY-----) добавляет стандартную защиту паролем.
Коротко о главном
PKCS#1 RSAPrivateKey | PKCS#8 PrivateKeyInfo / OneAsymmetricKey | |
|---|---|---|
| Спецификация | RFC 8017, приложение A.1.2 | RFC 5208, RFC 5958 |
| Метка PEM | RSA PRIVATE KEY | PRIVATE 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), слабая KDF | Стандартная схема PBES2 (PBKDF2 плюс AES) по RFC 8018 |
| Вывод OpenSSL 3 | Только с -traditional | По умолчанию для genpkey, pkey, rsa |
Java PKCS8EncodedKeySpec | Не принимается | Принимается |
| Go | x509.ParsePKCS1PrivateKey | x509.ParsePKCS8PrivateKey |
Node.js createPrivateKey | Принимается (формат определяется по метке PEM) | Принимается |
WebCrypto importKey | Не принимается | Формат pkcs8 |
Открытым ключам соответствуют PKCS#1 RSAPublicKey (BEGIN RSA PUBLIC KEY) и SubjectPublicKeyInfo (BEGIN PUBLIC KEY); всё остальное ПО рассчитано именно на второй формат.
Что внутри
Закрытый ключ RSA в формате PKCS#1 — это структура DER SEQUENCE, состоящая из версии, модуля n, открытой экспоненты e, закрытой экспоненты d, простых чисел p и q и CRT-параметров dP, dQ и qInv. Поскольку сама структура ничего не говорит об алгоритме, это должна делать метка 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 использует PBES2 с PBKDF2-HMAC-SHA256 и AES-256-CBC.
Конвертация
Скрипт ниже, запущенный с OpenSSL 3.6.5, показывает форматы по умолчанию и конвертацию туда и обратно без потерь.
#!/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
Те же команды конвертируют в PKCS#8 ключи EC в формате SEC 1 (BEGIN EC PRIVATE KEY). Закрытые ключи OpenSSH (BEGIN OPENSSH PRIVATE KEY) — третий формат; команда ssh-keygen -p -m PKCS8 -f key перезаписывает такой ключ на месте в формате PKCS#8.
Какие ошибки встретятся
Самые строгие требования к формату ключа у Java: PKCS8EncodedKeySpec принимает только PKCS#8, а текст ошибки менялся от выпуска к выпуску. Программа ниже загружает по одному PEM-файлу каждого вида, а команды после неё запускают её на Java 17, 21 и 25 в официальных образах Eclipse Temurin.
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);
}
}
}
}
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
== 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
Старые ответы в интернете цитируют текст Java 17; в Java 21 и 25 сообщение верхнего уровня — Unable to decode key, а «algid parse error, not a sequence» остаётся только в причине исключения (cause). Ищите в логах оба варианта.
Другие среды выполнения (воспроизведено в тот же день с ключами, сгенерированными 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): PKCS#1 в DER, объявленный какtype: "pkcs8", отклоняется с ошибкойerror:068000A8:asn1 encoding routines::wrong tag(кодERR_OSSL_ASN1_WRONG_TAG); тело PKCS#1 под меткойBEGIN PRIVATE KEY— с ошибкойerror:1E08010C:DECODER routines::unsupported; те же байты PKCS#1, переданные вcreatePublicKeyкакspki, даютFailed to read asymmetric key. Зашифрованный ключ, загруженный без парольной фразы, отклоняется с ошибкойerror:07880109:common libcrypto routines::interrupted or cancelled. - Ключи, зашифрованные устаревшими алгоритмами: традиционный PEM-ключ, зашифрованный DES, или ключ PKCS#8 под PBES1 с MD5 и DES загружается в OpenSSL 3 только с провайдером legacy. Без него
openssl pkeyвыводитerror:0308010C:digital envelope routines:inner_evp_generic_fetch:unsupportedи следом алгоритм, который не удалось получить (DES-CBCилиPBKDF1для PBES1), а Node.js сообщаетerror:0308010C:digital envelope routines::unsupported(ERR_OSSL_EVP_UNSUPPORTED). - Python (
cryptography46):load_pem_private_keyодинаково читает PKCS#1 и PKCS#8, а для зашифрованного ключа без пароля выбрасываетTypeError: Password was not given but private key is encrypted. - WebCrypto:
importKey("pkcs8", …)с байтами PKCS#1 завершается ошибкойDataError; в Node.js 26 текст ошибки —Invalid keyData, у браузеров — свой.
В каталоге ошибок есть страница для каждой из них; ссылки ниже.
То же разделение у открытых ключей
У открытых ключей те же две формы. PKCS#1 RSAPublicKey (BEGIN RSA PUBLIC KEY) содержит только модуль и экспоненту; SubjectPublicKeyInfo (BEGIN PUBLIC KEY, RFC 5280) оборачивает ключ вместе с идентификатором алгоритма, подходит для любого алгоритма, и именно его ожидают сертификаты, конвертеры JWK, X509EncodedKeySpec в Java, ParsePKIXPublicKey в Go и импорт spki в WebCrypto. Для конвертации между ними служат команды openssl rsa -pubin -in key.pem -RSAPublicKey_out и openssl pkey -pubin -in key.pem -pubout. Путаница возникает в обе стороны: инструменты, которые записывают SPKI, часто называют файл в стиле id_rsa.pub, а собственные однострочные открытые ключи OpenSSH (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): PKCS#1 через-traditional, в идеале только в момент передачи ключа такой программе. - Ключ вместе с сертификатом для Windows, macOS или хранилищ ключей Java: PKCS#12, внутри которого ключи хранятся в PKCS#8.
- Ключи SSH: формат OpenSSH для самого
ssh; PKCS#8, если тот же ключ нужен другому инструменту.
Заметки о миграции
- Найдите файлы PKCS#1 и SEC 1:
grep -l "BEGIN RSA PRIVATE KEY\|BEGIN EC PRIVATE KEY"по репозиториям конфигурации и хранилищам секретов. - Конвертируйте один раз командой
openssl pkcs8 -topk8 -nocrypt(или-v2 aes-256-cbc, чтобы зашифровать) и обновите все программы, которые используют ключ; ключевой материал и соответствующий сертификат не меняются. - Перешифруйте старые зашифрованные ключи: традиционное шифрование PEM (заголовки
Proc-TypeиDEK-Info) получает ключ за одну итерацию MD5, а для ключей, зашифрованных DES или RC2 в любом из форматов, нужен провайдер legacy в OpenSSL; перешифруйте их с PBES2 и AES-256. - Переведите код на универсальные парсеры (
ParsePKCS8PrivateKey,load_pem_private_key,createPrivateKey), чтобы новые алгоритмы, включая Ed25519 и ML-DSA, работали без изменения кода. - Проверьте результат: подпишите данные преобразованным ключом и проверьте подпись по существующему сертификату; генератор ключей и просмотр структуры ASN.1 показывают структуру, ничего не отправляя на сервер.