Ключи для Payin
Ключи для Payin
Все запросы и ответы Payin подписываются по алгоритму ed25519 (RFC 8032). Библиотеки для ed25519 есть практически во всех языках.
Key-Id и Signature. В Payin H2H те же значения передаются в заголовках X-KeyId и X-Signature. Алгоритм подписи, формат ключей и порядок проверки полностью идентичны — отличается только название заголовков.Как считается подпись
- Для HTTP POST — подпись вычисляется из тела запроса (
request body). - Для WebSocket — подпись вычисляется от значения поля
bodyсообщения.
Результат кодируется в Base64 и передаётся:
- в HTTP — в заголовке
Signature(Payin Classic) илиX-Signature(Payin H2H); - в WebSocket — в поле
Signatureсообщения.
Вместе с подписью передаётся идентификатор публичного ключа: Key-Id (Payin Classic) или X-KeyId (Payin H2H). Значение фиксированное и выдаётся вручную до начала работы.
Разные пары ключей для запросов и ответов
| Направление | Приватный ключ | Публичный ключ |
|---|---|---|
| Клиент → сервер (ваши запросы) | у клиента | у сервера (вы передаёте его нам вручную) |
| Сервер → клиент (наши ответы) | у сервера | у клиента (мы передаём его вам вручную) |
Все ключи и подписи ходят в Base64.
Генерация ключей
Ключи вы генерируете у себя — приватный ключ никогда не покидает вашу инфраструктуру.
Скрипт ниже создаёт в текущей папке два файла:
| Файл | Что внутри | Что с ним делать |
|---|---|---|
private.key | приватный ключ ed25519 (64 байта) в Base64 | хранить у себя, права 600, никому не передавать |
public.key | публичный ключ ed25519 (32 байта) в Base64 | передать нам до начала работы |
Содержимое private.key — это ровно та Base64-строка, которую ждёт конфиг SDK, а public.key — то, что вы присылаете нам.
Прямо здесь, в браузере
private.key и public.keyКлючи создаёт ваш браузер, наружу они не уходят. Страницу можно сохранить и открыть без интернета.
Скриптом
Если генерировать ключи в браузере нельзя по вашим правилам безопасности — тот же результат даёт короткий скрипт. Он перезапишет существующие private.key и public.key без предупреждения, поэтому запускайте его в пустой папке.
// сохраните как main.go и запустите: go run main.go
package main
import (
"crypto/ed25519"
"crypto/rand"
"encoding/base64"
"os"
)
func main() {
pub, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
panic(err)
}
if err := os.WriteFile("private.key", []byte(base64.StdEncoding.EncodeToString(priv)), 0o600); err != nil {
panic(err)
}
if err := os.WriteFile("public.key", []byte(base64.StdEncoding.EncodeToString(pub)), 0o644); err != nil {
panic(err)
}
}
// сохраните как generate_keys.mjs и запустите: node generate_keys.mjs
import { generateKeyPairSync } from 'node:crypto'
import { writeFileSync } from 'node:fs'
const { privateKey, publicKey } = generateKeyPairSync('ed25519')
const pubRaw = publicKey.export({ type: 'spki', format: 'der' }).slice(12)
const privRaw = privateKey.export({ type: 'pkcs8', format: 'der' }).slice(16)
const privFull = Buffer.concat([privRaw, pubRaw])
writeFileSync('private.key', privFull.toString('base64'), { mode: 0o600 })
writeFileSync('public.key', pubRaw.toString('base64'))
# pip install cryptography
# сохраните как generate_keys.py и запустите: python generate_keys.py
import base64
import os
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
private_key = Ed25519PrivateKey.generate()
priv_raw = private_key.private_bytes_raw()
pub_raw = private_key.public_key().public_bytes_raw()
descriptor = os.open("private.key", os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
with open(descriptor, "w") as file:
file.write(base64.b64encode(priv_raw + pub_raw).decode())
with open("public.key", "w") as file:
file.write(base64.b64encode(pub_raw).decode())
<?php
// сохраните как generate_keys.php и запустите: php generate_keys.php
$keypair = sodium_crypto_sign_keypair();
file_put_contents('private.key', base64_encode(sodium_crypto_sign_secretkey($keypair)));
chmod('private.key', 0600);
file_put_contents('public.key', base64_encode(sodium_crypto_sign_publickey($keypair)));
openssl для генерации ed25519-ключей — утилита добавляет в файл служебные байты, которые сломают подпись.Через SDK
Те же два файла создаёт готовый скрипт из SDK. В отличие от скриптов выше он остановится и ничего не перезапишет, если private.key или public.key уже существует:
go run examples/generate_keys/main.go
node examples/generate_keys.js
python examples/generate_keys.py
php examples/generate_keys.php
Формирование подписи
Подпись считается тем самым приватным ключом, который лежит в private.key — файл содержит Base64, поэтому перед использованием его нужно раскодировать.
$sign_secret = base64_decode(trim(file_get_contents('./private.key')));
$message = '{"Id":"1643731972","Kind":"BankCard","Currency":"RUB","Sum100":10000}';
$signature = sodium_crypto_sign_detached($message, $sign_secret);
$signature_base64 = base64_encode($signature);
// $signature_base64 -> заголовок "Signature"
// $message -> тело HTTP-запроса
import (
"crypto/ed25519"
"encoding/base64"
"os"
"strings"
)
raw, err := os.ReadFile("private.key")
if err != nil {
// обработка ошибки
}
privB, err := base64.StdEncoding.DecodeString(strings.TrimSpace(string(raw)))
if err != nil || len(privB) != ed25519.PrivateKeySize {
// обработка ошибки
}
privK := ed25519.PrivateKey(privB)
msg := `{"Id":"1643731972","Kind":"BankCard","Currency":"RUB","Sum100":10000}`
signatureBase64 := base64.StdEncoding.EncodeToString(ed25519.Sign(privK, []byte(msg)))
// signatureBase64 -> заголовок "Signature"
// msg -> тело HTTP-запроса
import { createPrivateKey, sign } from 'node:crypto'
import { readFileSync } from 'node:fs'
const raw = Buffer.from(readFileSync('./private.key', 'utf8').trim(), 'base64')
// Node принимает ed25519-ключ только в DER PKCS#8 — собираем его из первых 32 байт (seed)
const der = Buffer.concat([
Buffer.from('302e020100300506032b657004220420', 'hex'),
raw.subarray(0, 32),
])
const privateKey = createPrivateKey({ key: der, format: 'der', type: 'pkcs8' })
const message = JSON.stringify({ Id: '1643731972', Kind: 'BankCard', Currency: 'RUB', Sum100: 10000 })
const signature = sign(null, Buffer.from(message), privateKey).toString('base64')
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
import base64
with open('./private.key') as file:
raw = base64.b64decode(file.read().strip())
private_key = Ed25519PrivateKey.from_private_bytes(raw[:32])
message = b'{"Id":"1643731972","Kind":"BankCard","Currency":"RUB","Sum100":10000}'
signature = private_key.sign(message)
signature_b64 = base64.b64encode(signature).decode()
Проверка ответа
Сервер присылает заголовки Key-Id и Signature (в Payin H2H — X-KeyId и X-Signature). Декодируйте подпись из Base64 и проверьте её для тела ответа нашим публичным ключом (который вы получили вручную).