Цель

Цель теста — проверить базовую интеграцию с HSM по REST: доступность API, аутентификацию, вызов операции sign, корректную обработку ответа и верификацию результата на стороне клиента. В практических HSM REST API встречается именно такой шаблон: POST запрос с JSON, выбор ключа по идентификатору/label, возврат подписи в JSON и работа по TLS или mTLS. [nshielddocs.entrust](https://nshielddocs.entrust.com/wsop-docs/v3.2.0/rest-api.html)

Что проверяет пример:

Код реализации

Ниже пример для тестовой задачи. Он рассчитан на абстрактный REST API вида POST /v1/sign, потому что у разных вендоров формат немного отличается, но сама схема запроса типовая. [nshielddocs.entrust](https://nshielddocs.entrust.com/wsop-docs/v3.2.0/rest-api.html)

#!/usr/bin/env python3
import base64
import hashlib
import json
import requests

from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec, padding
from cryptography.exceptions import InvalidSignature

# =========================
# Конфигурация
# =========================
HSM_BASE_URL = "https://hsm.example.local"
HSM_SIGN_ENDPOINT = f"{HSM_BASE_URL}/v1/sign"

API_TOKEN = "replace_me_with_real_token"
KEY_ID = "test-sign-key-01"
ALGORITHM = "RSA_PKCS1_SHA256"   # или ECDSA_SHA256, зависит от HSM

VERIFY_TLS = True  # или путь к CA-файлу, например "/etc/ssl/certs/hsm-ca.pem"

PUBLIC_KEY_PEM = b"""
-----BEGIN PUBLIC KEY-----
REPLACE_WITH_REAL_PUBLIC_KEY
-----END PUBLIC KEY-----
"""

# =========================
# Подготовка сообщения
# =========================
message = b"test payload for HSM REST signing"
message_digest = hashlib.sha256(message).digest()
message_digest_b64 = base64.b64encode(message_digest).decode("ascii")

# =========================
# Формирование запроса
# =========================
payload = {
    "key_id": KEY_ID,
    "algorithm": ALGORITHM,
    "hash_b64": message_digest_b64,
    "hash_algorithm": "SHA256"
}

headers = {
    "Authorization": f"Bearer {API_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
}

# =========================
# Вызов HSM REST API
# =========================
response = requests.post(
    HSM_SIGN_ENDPOINT,
    headers=headers,
    data=json.dumps(payload),
    timeout=15,
    verify=VERIFY_TLS,
)

response.raise_for_status()
result = response.json()

# Ожидаем, что HSM вернет подпись в base64
signature_b64 = result["signature_b64"]
signature = base64.b64decode(signature_b64)

print("HSM response received successfully")
print("Key ID:", result.get("key_id", KEY_ID))
print("Algorithm:", result.get("algorithm", ALGORITHM))
print("Signature length:", len(signature))

# =========================
# Локальная проверка подписи
# =========================
public_key = serialization.load_pem_public_key(PUBLIC_KEY_PEM)

try:
    if ALGORITHM == "RSA_PKCS1_SHA256":
        public_key.verify(
            signature,
            message_digest,
            padding.PKCS1v15(),
            hashes.SHA256()
        )

    elif ALGORITHM == "ECDSA_SHA256":
        public_key.verify(
            signature,
            message_digest,
            ec.ECDSA(hashes.SHA256())
        )

    else:
        raise ValueError(f"Unsupported algorithm for local verify: {ALGORITHM}")

    print("Signature verification: OK")

except InvalidSignature:
    print("Signature verification: FAILED")
    raise

Что адаптировать

Под конкретный HSM обычно нужно подправить JSON‑поля и схему аутентификации. Например, некоторые REST API используют keylabel в URL или принимают не хэш, а исходные данные в JSON, после чего хэширование выполняется уже на стороне HSM. [a-trust](https://www.a-trust.at/docs/rk/asignRKHSMDeveloper_en.pdf)

Чаще всего меняются вот эти вещи:

Тестовый FastAPI mock

Если у тебя пока нет реального HSM или эмулятора, удобно сначала поднять локальный mock REST‑сервис и отладить клиент на нём. Это не HSM, а заглушка для тестирования интеграционного кода, но подход полезен до подключения настоящего модуля. Реальные решения тоже строятся вокруг REST‑вызова, выбора ключа и возврата JSON‑ответа с подписью. [linkedin](https://www.linkedin.com/posts/rizkysatrio_github-rsatriosofthsm2-rest-rest-api-activity-7248141405200515072-V2Zc)

#!/usr/bin/env python3
from fastapi import FastAPI, Header, HTTPException
from pydantic import BaseModel
import base64
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa, padding

app = FastAPI()

private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
public_key = private_key.public_key()

class SignRequest(BaseModel):
    key_id: str
    algorithm: str
    hash_b64: str
    hash_algorithm: str

@app.post("/v1/sign")
def sign(req: SignRequest, authorization: str | None = Header(default=None)):
    if authorization != "Bearer replace_me_with_real_token":
        raise HTTPException(status_code=401, detail="Unauthorized")

    if req.algorithm != "RSA_PKCS1_SHA256":
        raise HTTPException(status_code=400, detail="Unsupported algorithm")

    digest = base64.b64decode(req.hash_b64)

    signature = private_key.sign(
        digest,
        padding.PKCS1v15(),
        hashes.SHA256()
    )

    return {
        "key_id": req.key_id,
        "algorithm": req.algorithm,
        "signature_b64": base64.b64encode(signature).decode("ascii"),
        "public_key_pem": public_key.public_bytes(
            encoding=serialization.Encoding.PEM,
            format=serialization.PublicFormat.SubjectPublicKeyInfo
        ).decode("ascii")
    }

Такой mock можно запустить через uvicorn mock_hsm:app --host 127.0.0.1 --port 8443, а затем направить основной клиент на http://127.0.0.1:8443/v1/sign`. Для боевого HSM потом останется заменить URL, схему авторизации и формат полей под документацию конкретного устройства. [linkedin](https://www.linkedin.com/posts/rizkysatrio_github-rsatriosofthsm2-rest-rest-api-activity-7248141405200515072-V2Zc)

* example ****
mock HSM REST сервер и клиент для теста подписи. В комплекте сервер поднимает REST‑эндпоинты для health, выдачи публичного ключа и подписи, а клиент получает публичный ключ, отправляет SHA‑256 хэш на подпись и локально проверяет возвращённую подпись. [docs.securosys](https://docs.securosys.com/tsb/overview/)

Что внутри

  • mock_hsm.py — FastAPI‑сервер с эндпоинтами /v1/health, /v1/public-key и /v1/sign, моделью Bearer‑авторизации и RSA‑подписью тестового дайджеста. Такой шаблон соответствует типовой схеме HSM REST‑интеграций, где клиент вызывает REST‑метод подписи и получает JSON‑ответ с результатом. [nshielddocs.entrust](https://nshielddocs.entrust.com/wsop-docs/v3.2.0/rest-api.html)
  • client_test.py — клиент на Python с requests и cryptography, который получает публичный ключ, вызывает REST‑подпись и локально валидирует её. Это повторяет типичный интеграционный smoke‑test для HSM API. [docs.venafi](https://docs.venafi.com/Docs/currentSDK/TopNav/Content/SDK/HSMSDK/cco-SDKh-HSM-Client-SDK-reference.php)

Как запускать

1. Установи зависимости: pip install fastapi uvicorn requests cryptography pydantic.
2. Запусти сервер: uvicorn mock_hsm:app --host 127.0.0.1 --port 8000.
3. Во втором терминале запусти клиент: python3 client_test.py.

Оба файла уже готовы к локальному запуску на Linux; токен в обоих файлах одинаковый — replace_me_with_real_token, так что клиент сразу совместим с mock‑сервером. [linkedin](https://www.linkedin.com/posts/rizkysatrio_github-rsatriosofthsm2-rest-rest-api-activity-7248141405200515072-V2Zc)

Что увидишь

При успешном запуске клиент выведет подтверждение получения публичного ключа, запроса подписи и успешной локальной проверки подписи. Такой тест подтверждает, что REST‑метод подписи, обмен JSON и базовая криптографическая логика работают корректно. [docs.securosys](https://docs.securosys.com/tsb/overview/)

######## mock_hsm.py ############
from fastapi import FastAPI, Header, HTTPException
from pydantic import BaseModel
import base64
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa, padding

app = FastAPI(title="Mock HSM REST API")

private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
public_key = private_key.public_key()
API_TOKEN = "replace_me_with_real_token"

class SignRequest(BaseModel):
key_id: str
algorithm: str
hash_b64: str
hash_algorithm: str

@app.get("/v1/health")
def health():
return {"status": "ok"}

@app.get("/v1/public-key")
def get_public_key(authorization: str | None = Header(default=None)):
if authorization != f"Bearer {API_TOKEN}":
raise HTTPException(status_code=401, detail="Unauthorized")
return {
"algorithm": "RSA_PKCS1_SHA256",
"public_key_pem": public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
).decode("ascii"),
}

@app.post("/v1/sign")
def sign(req: SignRequest, authorization: str | None = Header(default=None)):
if authorization != f"Bearer {API_TOKEN}":
raise HTTPException(status_code=401, detail="Unauthorized")

if req.algorithm != "RSA_PKCS1_SHA256":
raise HTTPException(status_code=400, detail="Unsupported algorithm")

if req.hash_algorithm != "SHA256":
raise HTTPException(status_code=400, detail="Unsupported hash algorithm")

try:
digest = base64.b64decode(req.hash_b64)
except Exception as exc:
raise HTTPException(status_code=400, detail=f"Invalid base64 hash: {exc}")

if len(digest) != 32:
raise HTTPException(status_code=400, detail="SHA256 digest must be 32 bytes")

signature = private_key.sign(
digest,
padding.PKCS1v15(),
hashes.SHA256(),
)

return {
"key_id": req.key_id,
"algorithm": req.algorithm,
"hash_algorithm": req.hash_algorithm,
"signature_b64": base64.b64encode(signature).decode("ascii"),
}

############ client_test.py #############
import base64
import hashlib
import json
import requests
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.exceptions import InvalidSignature

BASE_URL = "http://127.0.0.1:8000"
API_TOKEN = "replace_me_with_real_token"
KEY_ID = "test-sign-key-01"
ALGORITHM = "RSA_PKCS1_SHA256"
MESSAGE = b"test payload for HSM REST signing"

HEADERS = {
"Authorization": f"Bearer {API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
}

def get_public_key_pem():
r = requests.get(f"{BASE_URL}/v1/public-key", headers=HEADERS, timeout=10)
r.raise_for_status()
return r.json()["public_key_pem"].encode("ascii")

def sign_message_digest(message: bytes):
digest = hashlib.sha256(message).digest()
payload = {
"key_id": KEY_ID,
"algorithm": ALGORITHM,
"hash_b64": base64.b64encode(digest).decode("ascii"),
"hash_algorithm": "SHA256",
}
r = requests.post(
f"{BASE_URL}/v1/sign",
headers=HEADERS,
data=json.dumps(payload),
timeout=10,
)
r.raise_for_status()
body = r.json()
return digest, base64.b64decode(body["signature_b64"]), body

def verify_signature(public_key_pem: bytes, digest: bytes, signature: bytes):
public_key = serialization.load_pem_public_key(public_key_pem)
public_key.verify(
signature,
digest,
padding.PKCS1v15(),
hashes.SHA256(),
)

def main():
print("[*] Fetching public key from mock HSM...")
public_key_pem = get_public_key_pem()

print("[*] Requesting signature from mock HSM...")
digest, signature, body = sign_message_digest(MESSAGE)

print("[*] Verifying signature locally...")
try:
verify_signature(public_key_pem, digest, signature)
print("[OK] Signature verification successful")
print(f"Key ID: {body['key_id']}")
print(f"Algorithm: {body['algorithm']}")
print(f"Hash algorithm: {body['hash_algorithm']}")
print(f"Signature length: {len(signature)} bytes")
except InvalidSignature:
print("[FAIL] Signature verification failed")
raise

if __name__ == "__main__":
main()

##############################################
docker-compose.yml и Dockerfile для запуска mock HSM REST сервера в контейнере. Теперь сервер можно поднять через Docker Compose без ручной установки FastAPI и Uvicorn на хосте. [linkedin](https://www.linkedin.com/posts/rizkysatrio_github-rsatriosofthsm2-rest-rest-api-activity-7248141405200515072-V2Zc)

Как использовать

Положи docker-compose.yml, Dockerfile.mock-hsm и mock_hsm.py в один каталог, затем запусти docker compose up --build. После старта сервис будет доступен на http://127.0.0.1:8000, что совпадает с адресом в твоём client_test.py`. [linkedin](https://www.linkedin.com/posts/rizkysatrio_github-rsatriosofthsm2-rest-rest-api-activity-7248141405200515072-V2Zc)

Важный момент

В текущем docker-compose.yml токен задан через переменную окружения, но исходный mock_hsm.py из комплекта использует захардкоженное значение replace_me_with_real_token. Чтобы реально читать токен из контейнерного окружения, нужно добавить в mock_hsm.py чтение os.getenv("API_TOKEN", "replace_me_with_real_token"); без этого compose всё равно будет работать, но именно с дефолтным токеном из кода. [linkedin](https://www.linkedin.com/posts/rizkysatrio_github-rsatriosofthsm2-rest-rest-api-activity-7248141405200515072-V2Zc)

Что лучше поправить

Рекомендую сделать две мелкие доработки:

  • В mock_hsm.py заменить константу API_TOKEN на чтение из переменной окружения через os.getenv.
  • При желании добавить volume‑mount, чтобы mock_hsm.py не копировался в образ, а читался прямо из рабочего каталога во время разработки.

#######################

docker-compose.yml

#######################
services:
mock-hsm:
build:
context: .
dockerfile: Dockerfile.mock-hsm
container_name: mock-hsm
ports:

  • "8000:8000"

environment:
API_TOKEN: replace_me_with_real_token
command: uvicorn mock_hsm:app --host 0.0.0.0 --port 8000

#######################

Dockerfile

#######################
FROM python:3.12-slim

WORKDIR /app

COPY mock_hsm.py /app/mock_hsm.py

RUN pip install --no-cache-dir fastapi uvicorn cryptography pydantic

EXPOSE 8000

CMD ["uvicorn", "mock_hsm:app", "--host", "0.0.0.0", "--port", "8000"]

[file-name 000370_2026-06-04_07-51-26.txt]