PEM-формат: структура и разбор

PEM (Privacy Enhanced Mail) — текстовый формат представления криптографических объектов, основанный на кодировании бинарных данных в Base64 с добавлением заголовков и разделителей. В JavaScript-библиотеке Jsrsasign PEM используется как основной способ передачи ключей, сертификатов и других ASN.1-структур.

Ключевая идея PEM заключается в том, что бинарный DER-объект преобразуется в удобочитаемую строку, пригодную для хранения в файлах и передачи через текстовые протоколы.

Типичная форма PEM-объекта:

-----BEGIN CERTIFICATE-----
MIIC+TCCAeGgAwIBAgIBADANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDDApUZXN0
...
-----END CERTIFICATE-----

Основные элементы:

  • заголовочная строка BEGIN
  • Base64-кодированные данные
  • завершающая строка END
  • отсутствие бинарных символов
  • разбиение строк обычно по 64 символа

Jsrsasign рассматривает PEM как контейнер, из которого извлекается DER-структура для последующего ASN.1-разбора.


Базовая структура PEM

PEM можно разложить на логические части:

1. Заголовок

Формат:

-----BEGIN <TYPE>-----

Тип определяет содержимое:

  • CERTIFICATE — X.509 сертификат
  • PUBLIC KEY — открытый ключ
  • PRIVATE KEY — приватный ключ (PKCS#8)
  • RSA PRIVATE KEY — PKCS#1 RSA ключ

2. Тело (Base64)

Основная часть — это DER, закодированный в Base64.

Характерные особенности:

  • отсутствие пробелов внутри блоков (кроме переносов строк)
  • часто фиксированная ширина 64 символа
  • игнорируются символы перевода строки при декодировании

3. Завершение

-----END <TYPE>-----

Тип должен строго совпадать с BEGIN.


DER внутри PEM

PEM — это лишь упаковка. Реальные данные находятся в формате DER (Distinguished Encoding Rules), который является бинарным представлением ASN.1-структур.

Jsrsasign работает следующим образом:

  1. PEM → удаление заголовков
  2. Base64 decode
  3. получение DER (hex или binary)
  4. ASN.1 парсинг

PEM в Jsrsasign: базовый разбор

В Jsrsasign ключевую роль играет объект KEYUTIL.

Пример извлечения ключа:

const key = KEYUTIL.getKey(pemString);

Что происходит внутри:

  • определяется тип PEM
  • извлекается Base64
  • декодируется DER
  • определяется структура (RSA, EC, PKCS#8)
  • возвращается объект ключа

Разновидности PEM-форматов

X.509 сертификат

Содержит публичный ключ и метаданные.

Структура:

  • tbsCertificate
  • signatureAlgorithm
  • signatureValue

В Jsrsasign:

const x509 = new X509();
x509.readCertPEM(pemCert);

Доступ к данным:

x509.getSubjectString();
x509.getIssuerString();
x509.getPublicKey();

PKCS#1 RSA PRIVATE KEY

Начинается с:

-----BEGIN RSA PRIVATE KEY-----

ASN.1 структура:

  • version
  • modulus (n)
  • publicExponent (e)
  • privateExponent (d)
  • primes (p, q)
  • exponents
  • coefficient

Jsrsasign автоматически распознаёт этот формат через KEYUTIL.


PKCS#8 PRIVATE KEY

Универсальный формат:

-----BEGIN PRIVATE KEY-----

или зашифрованный:

-----BEGIN ENCRYPTED PRIVATE KEY-----

Структура:

  • version
  • algorithm identifier
  • privateKey OCTET STRING

В Jsrsasign:

const key = KEYUTIL.getKey(pemPrivateKey);

PUBLIC KEY

-----BEGIN PUBLIC KEY-----

Содержит SubjectPublicKeyInfo:

  • algorithm
  • public key bit string

PEM с дополнительными заголовками

Некоторые PEM включают дополнительные метаданные:

Proc-Type: 4,ENCRYPTED
DEK-Info: AES-256-CBC,AB12CD34EF...

Jsrsasign использует эти поля для:

  • определения алгоритма шифрования
  • извлечения IV
  • расшифровки приватного ключа

Декодирование PEM в Jsrsasign

Шаг 1: очистка строки

Удаляются:

  • BEGIN/END строки
  • переносы строк
  • пробелы

Шаг 2: Base64 → DER

Внутренние функции:

  • b64tohex
  • hextorstr

Результат — hex-представление DER.


Шаг 3: ASN.1 парсинг

Используется модуль:

  • ASN1HEX

Пример:

const hex = pemToHex(pem);
const seq = ASN1HEX.getChildIdx(hex, 0);

Структура ASN.1 после декодирования

Пример сертификата:

SEQUENCE
 ├── TBSCertificate
 │    ├── Version
 │    ├── SerialNumber
 │    ├── Signature
 │    ├── Issuer
 │    ├── Validity
 │    ├── Subject
 │    └── SubjectPublicKeyInfo
 ├── SignatureAlgorithm
 └── SignatureValue

Jsrsasign предоставляет методы для обхода дерева без ручного парсинга.


Проверка типа PEM

Jsrsasign определяет тип автоматически:

KEYUTIL.getKey(pem)

или вручную:

KEYUTIL.getPEMType(pem)

Возможные значения:

  • PKCS1PRV
  • PKCS8PRV
  • X509CERT
  • PUBKEY

Ошибки при работе с PEM

1. Несоответствие BEGIN/END

Если:

-----BEGIN CERTIFICATE-----
...
-----END PUBLIC KEY-----

парсер выдаёт ошибку структуры.


2. Повреждённый Base64

Причины:

  • лишние символы
  • обрезанные строки
  • неправильная кодировка файла

3. Несоответствие формата ключа

Например, RSA ключ передан как EC.

Jsrsasign не сможет построить корректный объект.


Преобразование PEM в другие форматы

PEM → HEX

const hex = KJUR.asn1.ASN1Util.pemToHex(pem);

PEM → DER (binary)

const der = Buffer.from(b64, 'base64');

PEM → KEY объект

const key = KEYUTIL.getKey(pem);

Генерация PEM в Jsrsasign

Приватный RSA ключ:

const kp = KEYUTIL.generateKeypair("RSA", 2048);
const pem = KEYUTIL.getPEM(kp.prvKeyObj, "PKCS1PRV");

Публичный ключ:

const pub = KEYUTIL.getPEM(kp.pubKeyObj);

Особенности форматирования PEM

  • строгое разделение строк улучшает совместимость
  • допустимы как LF, так и CRLF
  • Base64 может быть без padding в некоторых реализациях
  • Jsrsasign устойчив к разным вариантам разбиения строк

Внутренняя модель PEM в Jsrsasign

PEM рассматривается как:

PEM = header + base64(DER) + footer

DER затем интерпретируется как:

ASN.1 → структурированное дерево → криптографический объект

Использование PEM в криптографических операциях

После разбора PEM объект используется для:

  • подписи (RSA/ECDSA)
  • проверки подписи
  • шифрования
  • TLS-операций
  • JWT (JWS)

Пример подписи:

const sig = new KJUR.crypto.Signature({ "alg": "SHA256withRSA" });
sig.init(privateKey);
sig.updateString("data");
const hex = sig.sign();

Взаимосвязь PEM и PKI

PEM является базовым транспортным форматом в инфраструктуре открытых ключей:

  • сертификаты X.509
  • цепочки доверия
  • корневые CA
  • клиентские сертификаты

Jsrsasign использует PEM как универсальный входной слой для PKI-операций.