security: round 3 hardening (CSRF double-submit, TRX MITM, container hardening)
This commit is contained in:
@@ -2,15 +2,15 @@
|
||||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"title": "CryptoWallet API",
|
||||
"version": "4.0.0",
|
||||
"description": "Multi-chain crypto wallet API (non-custodial). Клиент сам деривит mnemonic и шлёт публичные адреса; сервер хранит только адреса и строит unsigned tx для отправки. Auth via JWT (cookie/Bearer), issued by external auth-service (BITOK)."
|
||||
"version": "5.0.0",
|
||||
"description": "Multi-chain custodial wallet API (ETH/BSC/BTC/TRX/SOL). Сервер генерит mnemonic, шифрует AES-256-GCM (master-key из HashiCorp Vault), хранит её и сам подписывает транзакции. Auth via JWT (cookie/Bearer), issued by external auth-service (BITOK)."
|
||||
},
|
||||
"servers": [
|
||||
{ "url": "/api", "description": "API root" }
|
||||
],
|
||||
"tags": [
|
||||
{ "name": "System", "description": "Health & service info" },
|
||||
{ "name": "Wallets", "description": "User wallet records" },
|
||||
{ "name": "Wallets", "description": "Custodial wallet lifecycle" },
|
||||
{ "name": "Wallet Ops", "description": "Per-chain balance / transactions / send" },
|
||||
{ "name": "BTC", "description": "Bitcoin RPC proxy (Blockstream)" },
|
||||
{ "name": "TRON", "description": "TRON RPC proxy (TronGrid)" },
|
||||
@@ -32,10 +32,6 @@
|
||||
"error": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"SuccessEmpty": {
|
||||
"type": "object",
|
||||
"properties": { "success": { "type": "boolean", "example": true } }
|
||||
},
|
||||
"HealthResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -52,26 +48,7 @@
|
||||
"properties": {
|
||||
"chain": { "$ref": "#/components/schemas/Chain" },
|
||||
"address": { "type": "string" },
|
||||
"derivationPath": { "type": "string", "description": "BIP32 path, например m/44'/60'/0'/0/0" }
|
||||
}
|
||||
},
|
||||
"WalletInput": {
|
||||
"type": "object",
|
||||
"required": ["chain", "address", "derivationPath"],
|
||||
"properties": {
|
||||
"chain": { "$ref": "#/components/schemas/Chain" },
|
||||
"address": { "type": "string", "maxLength": 64, "description": "Публичный адрес (chain-specific checksum-валидируется)" },
|
||||
"derivationPath": { "type": "string", "maxLength": 64, "description": "BIP32 m/.. (например m/44'/60'/0'/0/0)" }
|
||||
}
|
||||
},
|
||||
"CreateWalletsRequest": {
|
||||
"type": "object",
|
||||
"required": ["wallets"],
|
||||
"properties": {
|
||||
"wallets": {
|
||||
"type": "array", "minItems": 1, "maxItems": 20,
|
||||
"items": { "$ref": "#/components/schemas/WalletInput" }
|
||||
}
|
||||
"derivationPath": { "type": "string", "description": "BIP32 path" }
|
||||
}
|
||||
},
|
||||
"WalletsResponse": {
|
||||
@@ -81,6 +58,31 @@
|
||||
"data": { "type": "array", "items": { "$ref": "#/components/schemas/Wallet" } }
|
||||
}
|
||||
},
|
||||
"MnemonicResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": { "type": "boolean", "example": true },
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"mnemonic": { "type": "string", "description": "BIP39 mnemonic (12 words)" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"TxBroadcastResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": { "type": "boolean", "example": true },
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"txid": { "type": "string", "description": "Идентификатор отправленной транзакции" },
|
||||
"chain": { "$ref": "#/components/schemas/Chain" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"BalanceResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -125,17 +127,7 @@
|
||||
"properties": {
|
||||
"to": { "type": "string", "description": "Recipient address" },
|
||||
"amount": { "type": "string", "description": "Amount в smallest units" },
|
||||
"token": { "type": "string", "nullable": true, "description": "Например USDT для TRC20/ERC20/BEP20. Без token = native (TRX/ETH/BNB/BTC)" }
|
||||
}
|
||||
},
|
||||
"UnsignedTxResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": { "type": "boolean" },
|
||||
"data": {
|
||||
"type": "object",
|
||||
"description": "Unsigned tx — формат зависит от chain (kind: btc | tron | evm | solana). Клиент подписывает приватом и broadcast'ит через соответствующий /api/{btc,tron}/broadcast endpoint или RPC своей цепи."
|
||||
}
|
||||
"token": { "type": "string", "nullable": true, "description": "USDT для TRC20/ERC20/BEP20. Без token = native." }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,17 +159,44 @@
|
||||
|
||||
"/wallets/create": {
|
||||
"post": {
|
||||
"summary": "Upsert wallets для авторизованного юзера",
|
||||
"description": "Клиент сам генерит mnemonic и деривит публичные адреса (BIP44 для ETH/BSC/BTC/TRX/SOL). Тело — массив `{chain, address, derivationPath}`. На конфликт (user_id, chain) сервер обновляет address+derivationPath. Mnemonic клиенту не нужно слать — сервер её не хранит.",
|
||||
"summary": "Создать custodial-кошелёк (server-side mnemonic)",
|
||||
"description": "**Тело запроса не требуется.** Сервер генерит BIP39 mnemonic (12 слов), деривит адреса для 5 chains (BIP44: ETH m/44'/60'/0'/0/0, BTC m/84'/0'/0'/0/0, TRX m/44'/195'/0'/0/0, SOL m/44'/501'/0'/0', BSC = ETH path), шифрует mnemonic AES-256-GCM (master-key из HashiCorp Vault) и атомарно сохраняет. **Возвращает ТОЛЬКО адреса** — mnemonic клиенту не отдаётся. Чтобы потом увидеть seed — отдельный endpoint POST /wallets/mnemonic/reveal. Идемпотентность: 409 если у юзера уже есть кошелёк.",
|
||||
"tags": ["Wallets"],
|
||||
"responses": {
|
||||
"201": { "description": "Wallet created (returns addresses only)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WalletsResponse" } } } },
|
||||
"401": { "description": "Not authenticated" },
|
||||
"409": { "description": "Wallet already exists", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
|
||||
"503": { "description": "Crypto service not ready" }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"/wallets/mnemonic/reveal": {
|
||||
"post": {
|
||||
"summary": "Раскрыть mnemonic (settings-screen)",
|
||||
"description": "Расшифровывает и возвращает 12-словную BIP39 мнемонику юзера. POST + CSRF + body-confirmation. Rate-limit 5/час per-user. Каждый запрос пишется в audit-log.",
|
||||
"tags": ["Wallets"],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateWalletsRequest" } } }
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["confirm"],
|
||||
"properties": {
|
||||
"confirm": { "type": "string", "enum": ["I_UNDERSTAND_SEED_IS_SECRET"] }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": { "description": "Created/updated (вернёт сохранённые адреса)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WalletsResponse" } } } },
|
||||
"400": { "description": "Invalid input (chain/address/derivationPath)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
|
||||
"401": { "description": "Not authenticated" }
|
||||
"200": { "description": "Mnemonic revealed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MnemonicResponse" } } } },
|
||||
"400": { "description": "Missing/invalid confirm token" },
|
||||
"401": { "description": "Not authenticated" },
|
||||
"404": { "description": "Wallet not created yet" },
|
||||
"429": { "description": "Rate limit (5/hour) exceeded" },
|
||||
"503": { "description": "Crypto service not ready" }
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -212,8 +231,8 @@
|
||||
|
||||
"/wallets/{chain}/send": {
|
||||
"post": {
|
||||
"summary": "Build unsigned send transaction (non-custodial)",
|
||||
"description": "Возвращает unsigned tx. Клиент подписывает приватным ключом и broadcast'ит через /api/{btc,tron}/broadcast или RPC своей цепи.",
|
||||
"summary": "Custodial send: server signs + broadcasts",
|
||||
"description": "Юзер на клиенте жмёт 'подтвердить' → клиент шлёт {to, amount, token?}. Сервер расшифровывает мнемонику, деривит chain privkey, подписывает, broadcast'ит. Возвращает txid. Защита: TRX MITM check, EVM gas cap 500 gwei, SOL confirmTransaction, BTC timeout + safety multiplier.",
|
||||
"tags": ["Wallet Ops"],
|
||||
"parameters": [{ "name": "chain", "in": "path", "required": true, "schema": { "$ref": "#/components/schemas/Chain" } }],
|
||||
"requestBody": {
|
||||
@@ -221,10 +240,11 @@
|
||||
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/SendRequest" } } }
|
||||
},
|
||||
"responses": {
|
||||
"200": { "description": "Unsigned tx", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UnsignedTxResponse" } } } },
|
||||
"200": { "description": "Broadcast successful", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TxBroadcastResponse" } } } },
|
||||
"400": { "description": "Invalid input" },
|
||||
"404": { "description": "Wallet not found" },
|
||||
"502": { "description": "Upstream RPC error" }
|
||||
"404": { "description": "Wallet/mnemonic not found" },
|
||||
"502": { "description": "Broadcast failed (insufficient balance / RPC error / unsupported)" },
|
||||
"503": { "description": "Crypto service not ready" }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user