niko_trust/docs/SERVICE-GUIDE.md
Niko Marmeladkov 3bf13fa488 Public SDK packages, proxy-aware rate limits, service login recipe
- internal/{address,identity,protocol,tce,transport,verify} -> pkg/ so
  external Go projects can import the verified core; invariant tests
  updated for the new paths
- Config.TrustProxy: key rate limiting by X-Forwarded-For when the relay
  sits behind a reverse proxy (off by default, header never trusted
  otherwise)
- examples/service + examples/approve: complete passwordless login round
  trip (mint request -> wallet approves -> local verify), run live in CI
- docs/SERVICE-GUIDE.md: the integration recipe
2026-08-26 12:49:54 +03:00

7 KiB
Raw Blame History

SERVICE-GUIDE.md — вход по Niko Trust в вашем сервисе

Рецепт для владельца сервиса: как принимать Niko Trust как основной способ аутентификации. Полный рабочий код — examples/service (сервис) и examples/approve (роль кошелька).

Модель в двух абзацах

Идентичность — это пара ключей Ed25519; адрес trust1… выведен из публичного ключа (bech32). Сервис не видит паролей: он публикует ApprovalRequest — подписанный вопрос «войти?» адресованный конкретному адресу, действительный ≤60 секунд. Владелец кошелька (приложение niko_trust_gui / Android) отвечает ApprovalResponse, который криптографически привязан к байтам именно этого запроса (INV-4). Релей — тупое хранилище конвертов: он не проверяет подписи и не знает, кто прав; всю верификацию сервис делает сам локально.

Что это даёт по безопасности: фишинг невозможен (запрос подписан ключом сервиса и показывается в кошельке с его адресом), повтор невозможен (ответ умирает вместе с окном запроса), подмена решения невозможна (ответ подписан ключом пользователя и привязан к хешу запроса), а компрометация релея не даёт атакующему ничего, кроме отказа в обслуживании.

Поток логина

браузер                ваш сервис                    релей          кошелёк
   |  GET /login?user=X    |                            |               |
   |----------------------->| ApprovalRequest(action=    |               |
   |                        |   "login", recipient=X)    |               |
   |                        |-- POST /v1/objects ------->|               |
   |<- { id } --------------|                            |--- push ----->|
   |                        |   GET /v1/responses?request=id (или WS)     |
   |   ...пользователь жмёт «Разрешить» в кошельке...    |<-- store -----|
   |  GET /login/status     |<-- envelope {tce,sig} -----|               |
   |----------------------->| VerifyApprovalResponse()   |               |
   |<- {approved, user:X} --|   → своя сессия            |               |

Шаги

1. Идентичность сервиса

Сгенерируйте один раз ключ и храните сид как секрет (это «сертификат» сервиса):

sv, _ := signer.Generate()
fmt.Println(sv.Address(), hex.EncodeToString(sv.Seed()))
// при старте: signer.FromSeed(seedBytes)

Адрес сервиса увидит пользователь в кошельке рядом с текстом запроса.

2. Создание запроса на вход

req := &protocol.ApprovalRequest{
    Sender:    sv.Public(),
    Recipient: []byte(userAddr.PubKey()), // чей это вход
    Action:    "login",
    Payload:   map[string]tce.Value{"session": tce.String(sessionID)},
    Message:   "Sign in to demo service", // человек это прочитает
    CreatedAt: uint64(time.Now().Unix()),
    ExpiresAt: uint64(time.Now().Unix() + 60),
    Nonce:     nonce16(),
}
tceBytes, _ := protocol.EncodeApprovalRequest(req)
sig := sv.Sign(tceBytes)
id := tce.ComputeID(tceBytes) // = object id, им же ссылается ответ
POST {base}/v1/objects {"tce": b64(tceBytes), "signature": b64(sig)}

Recipient обязан быть валидным адресом: запрос всегда адресный.

3. Ожидание ответа

Два способа:

  • Опрос (просто): GET /v1/responses?request=<hex id> каждые ~0.7 c. Требует сессию чтения — см. шаг 5.
  • WebSocket (правильно): подключиться к GET /v1/ws со своим токеном и отправить {"op":"subscribe","channel":"responses","key":"<hex id>"} — события приходят мгновенно (docs/API.md §WebSocket).

4. Верификация — единственная критичная строка

resp, err := protocol.VerifyApprovalResponse(reqTCE, reqSig, respTCE, respSig)
if err == nil && resp.Decision == protocol.Allow &&
    bytes.Equal(resp.Responder, []byte(userAddr.PubKey())) {
    // адрес верифицирован → выпустить свою сессию
}

Проверка уже включает: строгий декод обоих объектов, подпись под ключом ответившего, привязку к точным байтам вашего запроса и окно времени. Сверка Responder с ожидаемым адресом отсекает ответы чужих кошельков.

5. Сессия чтения релея

Чтение лент требует токена: POST /v1/auth/challenge → подписать AuthAssertion (audience взять из GET /v1/config) → POST /v1/auth/assertsession_token, живёт 30 минут (docs/API.md). Готовые реализации: pkg/protocol.VerifyAuthAssertion-клиенты в примерах выше.

Где брать готовый клиентский код

Язык Пакет
Go этот модуль: pkg/tce, pkg/protocol, pkg/verify, pkg/address, pkg/identity/signer
Rust крейт niko_trust_gui (git-зависимость), модули tce, protocol, relay; пример examples/service_login.rs
Kotlin/JVM модуль :sdk репозитория niko_trust_android (RelayClient, LiveFeed, протокол)

Эксплуатация

  • Релей за обратным прокси? Включите trust_proxy: true в config.yaml, иначе рейт-лимиты считают всех одним IP.
  • Рестарт релея сбрасывает сессии — клиенты перекладываются сами; ваш сервис должен переживать 502/401 во время окна рестарта.
  • Храните сид сервиса как секрет: он подписывает все запросы, и пользователь видит его адрес в каждом подтверждении.