niko_trust/docs/SERVICE-GUIDE.md
Niko Marmeladkov 1b6a1984c0 docs: QR-LOGIN reverse-mode standard
- docs/QR-LOGIN.md: canonical QR JSON contract (v/type/relay/svc/code/msg),
  reverse flow, client and service rules, security model (server can't
  substitute, code is a match+anti-phishing marker not an authenticator)
- SERVICE-GUIDE.md: link the two login modes
2026-08-26 17:51:04 +03:00

7.6 KiB
Raw Permalink 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). Релей — тупое хранилище конвертов: он не проверяет подписи и не знает, кто прав; всю верификацию сервис делает сам локально.

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

Есть два способа подтверждения:

  • Адресный (ниже): сервис уже знает trust1... пользователя и шлёт запрос на его кошелёк.
  • QR / reverse (без знания адреса): пользователь сканирует QR телефона и сам отправляет подтверждение сервису. Подходит и для входа, и для регистрации. Формат и правила — в QR-LOGIN.md.

Поток логина

браузер                ваш сервис                    релей          кошелёк
   |  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 во время окна рестарта.
  • Храните сид сервиса как секрет: он подписывает все запросы, и пользователь видит его адрес в каждом подтверждении.