Цель
Цель теста — проверить базовую интеграцию с 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‑эндпоинт доступен и отвечает JSON. [docs.securosys](https://docs.securosys.com/tsb/overview/)
- HSM подписывает данные по ключу, выбранному по
key_idилиkeylabel. [docs.venafi](https://docs.venafi.com/Docs/currentSDK/TopNav/Content/SDK/HSMSDK/cco-SDKh-HSM-Client-SDK-reference.php) - Закрытый ключ не покидает HSM; наружу возвращается только подпись. [docs.venafi](https://docs.venafi.com/Docs/currentSDK/TopNav/Content/SDK/HSMSDK/cco-SDKh-HSM-Client-SDK-reference.php)
- Клиент может локально проверить подпись по публичному ключу. Это удобная smoke‑проверка для стенда или эмулятора. [a-trust](https://www.a-trust.at/docs/rk/asignRKHSMDeveloper_en.pdf)
Код реализации
Ниже пример для тестовой задачи. Он рассчитан на абстрактный 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)
Чаще всего меняются вот эти вещи:
- URL эндпоинта:
/v1/sign,/sign,/keys/{keylabel}/signи т.д. [nshielddocs.entrust](https://nshielddocs.entrust.com/wsop-docs/v3.2.0/rest-api.html) - Аутентификация: Bearer token, client certificate, mTLS. У HSM REST это обычная практика. [docs.securosys](https://docs.securosys.com/tsb/overview/)
- Формат полей:
hash,data,to_be_signed,key_id,keylabel. [docs.venafi](https://docs.venafi.com/Docs/currentSDK/TopNav/Content/SDK/HSMSDK/cco-SDKh-HSM-Client-SDK-reference.php) - Формат подписи: Base64, Base64URL, ASN.1 DER, raw
R||Sдля ECDSA. [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:8000client_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]