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

121 lines
7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# SERVICE-GUIDE.md — вход по Niko Trust в вашем сервисе
Рецепт для владельца сервиса: как принимать Niko Trust как основной способ
аутентификации. Полный рабочий код — [`examples/service`](../examples/service)
(сервис) и [`examples/approve`](../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. Идентичность сервиса
Сгенерируйте один раз ключ и храните сид как секрет (это «сертификат»
сервиса):
```go
sv, _ := signer.Generate()
fmt.Println(sv.Address(), hex.EncodeToString(sv.Seed()))
// при старте: signer.FromSeed(seedBytes)
```
Адрес сервиса увидит пользователь в кошельке рядом с текстом запроса.
### 2. Создание запроса на вход
```go
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. Верификация — единственная критичная строка
```go
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/assert`
`session_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 во время окна рестарта.
- Храните сид сервиса как секрет: он подписывает все запросы, и пользователь
видит его адрес в каждом подтверждении.