Skip to content

SM9 — идентификационная криптография ​

SM9 — идентификационная криптосистема на билинейных спариваниях (серия GM/T 0044-2016). Её отличительная черта: идентификатор пользователя (например, email) и есть открытый ключ, поэтому цепочки сертификатов нет — центр генерации ключей (KGC) выводит приватный ключ каждой идентичности из главного секрета.

ВозможностьСтандартТочки входа
Цифровая подписьGM/T 0044.2sign / verify
Обмен ключамиGM/T 0044.3createExchangeInitiator / createExchangeResponder
KEM, шифрование и гибридное шифрованиеGM/T 0044.4encrypt / decrypt, kemEncrypt / kemDecrypt, hybridEncrypt / hybridDecrypt

Два главных ключа не взаимозаменяемы

Подпись и шифрование используют независимые главные ключи (purpose: 'sign' | 'enc'): главный открытый ключ подписи — точка G2 (128 байт), шифрования — точка G1 (64 байта). Использование не того назначения ломает проверку длины или спаривание.

Главные приватные ключи, главные открытые ключи и пользовательские приватные ключи кодируются в PEM по PKCS#8 с собственным OID 1.2.156.10197.1.100.

Импорт ​

ts
import { sm9 } from 'sm-crypto-wasm';

await sm9.ensureLoaded();

API ​

Генерация главной пары ключей sm9.generateKeyPair(opts) ​

ts
function generateKeyPair(opts: {
  id: string | Uint8Array;          // идентификатор пользователя
  purpose?: 'sign' | 'enc';          // по умолчанию 'sign'
}): Promise<Sm9KeyPair>

Возвращаются четыре части материала:

ПолеНазначениеФорма
masterPrivatePemглавный приватный ключ (подпись ks / шифрование ke)PKCS#8 PEM (скаляр 32 байта)
masterPublicPemглавный открытый ключ (подпись [ks]P2 / шифрование [ke]P1)SPKI PEM (G2 128B / G1 64B)
userPrivatePemприватный ключ пользователя (ds G1 64B / de G2 128B)PKCS#8 PEM
userPublicPemоткрытый ключ пользователя Q = [H1(id,hid)]P + PpubSPKI PEM

userPublicPem — детерминированная функция от (id, главный открытый ключ), а не независимый секрет: третья сторона может пересчитать его через getUserPublicKey.

Вывод открытых ключей ​

ФункцияСигнатураВозвращает
getSignPublicKey(ks: string | Uint8Array) => string128 байт в hex (G2, аффинные)
getEncPublicKey(ke: string | Uint8Array) => string64 байта в hex (G1, аффинные)
getUserPublicKey(masterPublicKey, id) => stringSPKI PEM

Все три принимают главный приватный ключ как 32 сырых байта (hex / Uint8Array / PKCS#8 PEM).

Подпись / проверка ​

ts
function sign(ks, id, msg, r?): string      // → подпись 96 байт в hex (h‖Sx‖Sy, 192 символа)
function verify(publicKey, id, msg, sig): boolean
ПараметрОписание
ks / publicKeyподпись принимает либо главный приватный ключ 32 байта (или PKCS#8 PEM), либо главный открытый ключ подписи 128 байт (или SPKI PEM) — различаются по длине
idидентификатор пользователя (строка или Uint8Array)
msgсообщение; строки кодируются в UTF-8
rнеобязательный одноразовый номер 32 байта (тестовая инъекция); иначе используется CSPRNG хоста

Шифрование / расшифрование ​

ts
function encrypt(publicKey, id, msg, r?): string      // → шифротекст в hex (C1 65B ‖ C3 32B ‖ C2)
function decrypt(ke, id, ct, opts?: { safeMode?: boolean }): string

Первым аргументом encrypt может быть главный приватный ключ шифрования 32 байта или главный открытый ключ шифрования 64 байта (SPKI PEM тоже подходит). decrypt требует главный приватный ключ шифрования (или выведенный приватный ключ пользователя); неудачная аутентификация бросает DecryptionError либо возвращает null при { safeMode: true }.

Инкапсуляция ключа (GM/T 0044.4) ​

ts
function kemEncrypt(ke, id, klen = 32, r?): { key: string; encapsulation: string }
function kemDecrypt(ke, id, encapsulation, klen = 32): string
ПараметрОписание
keглавный приватный ключ шифрования — KEM нужен он для вывода Ppub-e, в отличие от encrypt, который принимает и открытый ключ
idидентификатор получателя
klenдлина сессионного ключа в байтах, по умолчанию 32
encapsulationточка инкапсуляции 65 байт (04 ‖ x ‖ y) в hex

Гибридное шифрование (KEM + SM4-GCM) ​

ts
function hybridEncrypt(
  key, id, msg, opts?: { iv?: string | Uint8Array },
): string
function hybridDecrypt(
  ke, id, sealed, opts?: { safeMode?: boolean },
): string

key может быть 64-байтовым главным открытым ключом шифрования (SPKI PEM тоже подходит) или 32-байтовым главным приватным ключом шифрования (PKCS#8 PEM тоже подходит) — отправителю обычно нужен только главный открытый ключ. Один вызов выполняет KEM + SM4-GCM: KEM инкапсулирует 16-байтовый DEK, а сообщение обрабатывает SM4-GCM.

  • sealed использует тот же версионированный формат, что и SM2 hybrid: version(1) ‖ klen(1) ‖ encapLen(2) ‖ encapsulation ‖ iv(12) ‖ tag(16) ‖ ct.
  • Каждый вызов создаёт новые DEK и IV; строки msg интерпретируются как hex; для пустого сообщения передайте new Uint8Array(0).
  • В C1 у SM9 KEM нет C3/HMAC; целостность обеспечивает тег GCM. Неверный id / главный открытый ключ проявляется как DecryptionError в hybridDecrypt.
  • AAD привязан к encapsulation; при safeMode: true ошибки со стороны шифротекста возвращают null.
  • Гибридному режиму нужны и sm9.wasm, и sm4.wasm: перед первым вызовом выполните await sm9.ensureLoaded() и await sm4.ensureLoaded().
  • sealedToObject / sealedFromObject также доступны.

Обмен ключами (GM/T 0044.3) ​

ts
// Обе стороны должны принадлежать одному KGC и использовать один и тот же
// главный ключ шифрования ke; каждая передаёт свой ID и ID партнёра.
const initiator = await sm9.createExchangeInitiator(ke, idSelf, idPeer, opts?);
const responder = await sm9.createExchangeResponder(ke, idSelf, idPeer, opts?);

const m1 = initiator.initiate();          // фаза 1
const m2 = responder.respond(m1);         // фаза 2 (несёт confirm)
const m3 = initiator.confirm(m2);         // фаза 3
initiator.getSharedKey(m2, kLen?);        // обе стороны получают один сессионный ключ
responder.getSharedKey(m3, kLen?);

options.kLen задаёт длину сессионного ключа (по умолчанию 32); options.ephemeral — тестовая инъекция одноразового приватного ключа.

Использование ​

ts
import { sm9 } from 'sm-crypto-wasm';

const id = 'alice@example.com';

// по одной главной ключевой паре на назначение
const signKp = await sm9.generateKeyPair({ id });                 // purpose по умолчанию 'sign'
const encKp  = await sm9.generateKeyPair({ id, purpose: 'enc' });

// подпись / проверка
const sig = sm9.sign(signKp.masterPrivatePem, id, 'abc');
sm9.verify(signKp.masterPublicPem, id, 'abc', sig);                // → true

// шифрование / расшифрование
const ct = sm9.encrypt(encKp.masterPublicPem, id, 'abc');
sm9.decrypt(encKp.masterPrivatePem, id, ct);                       // → '616263'

// KEM: договариваемся о симметричном ключе, сообщение не передаётся
const { key, encapsulation } = sm9.kemEncrypt(encKp.masterPrivatePem, id, 32);
sm9.kemDecrypt(encKp.masterPrivatePem, id, encapsulation, 32);     // === key

Гибридное шифрование (подходит для больших сообщений):

ts
// Инкапсулировать 16-байтовый DEK главным открытым (или приватным) ключом + id,
// затем зашифровать сообщение через SM4-GCM.
const sealed = sm9.hybridEncrypt(encKp.masterPublicPem, id, '616263');
sm9.hybridDecrypt(encKp.masterPrivatePem, id, sealed);             // → '616263'

Playground ​

SM9: идентификационная криптография — подпись / шифрование / KEM

WASM · live

Первый запуск дополнительно создаёт две главные ключевые пары (для подписи и для шифрования, ~4 мс), затем выполняются подпись→проверка, шифрование→расшифрование, KEM инкапсуляция→декапсуляция и одно гибридное шифрование KEM + SM4-GCM. Идентификатор id *и есть* открытый ключ — цепочки сертификатов нет.

sm9.sign() → h‖Sx‖Sy—
sm9.verify()—
sm9.encrypt() → шифротекст (0 байт)—
sm9.decrypt() → открытый текст—
sm9.kemEncrypt() → сессионный ключ K—
sm9.kemDecrypt() → совпадает ли декапсулированный K?—
sm9.hybridEncrypt() → sealed (KEM + SM4-GCM)—
sm9.hybridDecrypt() → открытый текст—
Эквивалентный код
import { sm9 } from 'sm-crypto-wasm';

const signKp = await sm9.generateKeyPair({ id: 'alice@foo.com' });              // purpose по умолчанию 'sign'
const encKp  = await sm9.generateKeyPair({ id: 'alice@foo.com', purpose: 'enc' });

const sig = sm9.sign(signKp.masterPrivatePem, 'alice@foo.com', 'abc');
sm9.verify(signKp.masterPublicPem, 'alice@foo.com', 'abc', sig);

const ct = sm9.encrypt(encKp.masterPublicPem, 'alice@foo.com', 'abc');
sm9.decrypt(encKp.masterPrivatePem, 'alice@foo.com', ct);

const { key, encapsulation } = sm9.kemEncrypt(encKp.masterPrivatePem, 'alice@foo.com', 32);
sm9.kemDecrypt(encKp.masterPrivatePem, 'alice@foo.com', encapsulation, 32);      // === key
Почему SM9 медленнее остальных

Доминируют спаривания: одна подпись — примерно 4 мс, проверка — около 8 мс, шифрование — примерно 3,7 мс, на порядок больше SM2. Это математическая стоимость спариваний, а не недостаток реализации — см. страницу производительности.

Ошибки и граничные случаи ​

СитуацияБросается
id пустой в generateKeyPair / getUserPublicKeyRangeError
purpose не 'sign' и не 'enc'RangeError
главный приватный ключ не 32 байтаInvalidKeyError
длина подписи не 96 байтInvalidCiphertextError
шифротекст короче 97 байтInvalidCiphertextError
проверка HMAC по C3 не прошлаDecryptionError
encapsulation в kemDecrypt не 65 байтRangeError
hybridDecrypt: некорректная структура sealedInvalidCiphertextError
hybridDecrypt: проверка тега GCM не прошла (включая неверный id / главный открытый ключ)DecryptionError
hybridEncrypt: iv не 12 байтRangeError
klen не положительное целоеRangeError

Лицензия Apache-2.0