- 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
121 lines
7 KiB
Markdown
121 lines
7 KiB
Markdown
# 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 во время окна рестарта.
|
||
- Храните сид сервиса как секрет: он подписывает все запросы, и пользователь
|
||
видит его адрес в каждом подтверждении.
|