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