From 1b6a1984c0e613f33cf931c29210ce963bb1162d Mon Sep 17 00:00:00 2001 From: Niko Marmeladkov Date: Wed, 26 Aug 2026 17:51:04 +0300 Subject: [PATCH] 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 --- docs/QR-LOGIN.md | 115 ++++++++++++++++++++++++++++++++++++++++++ docs/SERVICE-GUIDE.md | 8 +++ 2 files changed, 123 insertions(+) create mode 100644 docs/QR-LOGIN.md diff --git a/docs/QR-LOGIN.md b/docs/QR-LOGIN.md new file mode 100644 index 0000000..c1cf1e1 --- /dev/null +++ b/docs/QR-LOGIN.md @@ -0,0 +1,115 @@ +# QR-LOGIN — Код-подтверждение входа по QR + +Стандарт на «reverse»-подтверждение: **телефон сканирует QR и сам отправляет +подтверждение** сервису, вместо того чтобы сервис заранее знал адрес +пользователя. Работает и для входа, и для регистрации, и не требует +изменений в протоколе TCE — используется обычный объект `ApprovalRequest`. + +Этот документ задаёт единый формат QR и правила взаимодействия: его обязаны +понимать (а) клиент-кошелёк (Android), который печатает QR на камеру и (б) +сервис, который QR показывает. + +## Формат QR + +QR кодирует компактный JSON (намеренно без пробелов): + +```json +{"v":1,"type":"nt-login","relay":"https://trust.n1ko.dev","svc":"trust1qz...","code":"482913","msg":"Вход в Forgejo"} +``` + +| Поле | Обязательно | Тип | Смысл | +|-------|-------------|--------------------|---------------------------------------------------------------| +| `v` | да | целое = `1` | версия формата; клиент отвергает незнакомое | +| `type`| да | строка = `nt-login`| тип QR; клиент игнорирует остальные | +| `relay`| да | URL | база релея (http/https/wss), куда класть подтверждение | +| `svc` | да | адрес `trust1...` | адрес сервиса, которому адресовано подтверждение (получатель) | +| `code`| да | ровно 6 цифр | одноразовый код для сверки (см. ниже) | +| `msg` | нет | строка ≤ 64 | человекочитаемое имя запроса, напр. «Вход в Forgejo» | + +Сервис, показывающий QR, сам генерирует `code` (случайно) и показывает его +рядом на экране крупно — это код анти-фишинга, а НЕ аутентификатор. + +## Поток + +``` + браузер/сервис телефон (клиент) релей + | показывает QR | | + | {relay, svc, code, msg} | | + |-------------------------------->| скан + показ диалога | + | | «msg? сервис short_svc» | + | | код 482913 (сверка) | + | пользователь жмёт Подтвердить | | + | | mint ApprovalRequest{ | + | | sender: я, | + | | recipient: svc, | + | | action: "login", | + | | payload:{code}, | + | | ttl ≤ 60s } | + | |-- store ------------------->| + | poll /v1/requests?recipient=svc| | + | <- envelope {tce,sig} ---------| | + | VerifyApprovalRequest | | + | match payload.code + свежесть | | + | user = req.sender -> сессия | | +``` + +Суть «reverse»-режима: подписанный `ApprovalRequest` от **пользователя** является +подтверждением личности. Сервису не нужно заранее знать адрес — он получает его +из проверенной подписи отправителя. + +## Правила для клиента (кошелька) + +1. Валидация QR: `v == 1`, `type == "nt-login"`, `relay` — валидный URL, + `svc` — валидный адрес `trust1...` (каноническая проверка public key), + `code` — ровно 6 цифр, `msg`, если есть, ≤ 64 символов. Любое нарушение — + отказать с объяснением. +2. Диалог подтверждения до отправки: + - заголовок = `msg` (если есть, иначе «Вход в сервис»); + - обязательно показать **короткий адрес сервиса** рядом с `msg` — имя + самозаявлено, ему не доверяют (см. INV-7, алиасы неавторитетны); + - крупно показать `code` для визуальной сверки с экраном браузера. +3. Отправить один `ApprovalRequest`: + - `sender` = ключ пользователя (подпись ставит клиент); + - `recipient` = `svc`; + - `action` = `"login"`; + - `payload` = `{"code": ""}` (код попадает внутрь подписи); + - `created_at` = now, `expires_at` = now + ≤60 (ограничение формата TCE); + - записать в историю. +4. Не показывать `code` в истории постоянно; пометить статусом «отправлено». + +## Правила для сервиса + +1. Сгенерировать одноразовый `code` и показывать QR вместе с ним. +2. Опрашивать `/v1/requests?recipient=<свой адрес>&after=<курсор>` + (или подписаться на канал `requests` по WS) со своей сессией чтения. +3. Для каждого конверта: `VerifyApprovalRequest` → проверить свежесть окна + (в границах `created_at..expires_at` за 60 с) → сопоставить + `payload.code` со своим активным QR. +4. Совпадение по `code` + прошедшая подпись ⇒ личность = `req.sender`. + Выпустить собственную сессию для этого адреса. +5. (Опционально) закрыть петлю: подписать `ApprovalResponse{Allow}` на этот + request и сохранить — клиент увидит «approved» в истории. + +## Безопасность + +- **Релей не может подставить адрес.** Подпись отправляет только владелец + ключа; релей видит всё, но подписать ничьим именем не может (максимум — + цензура/задержка, что неотличимо от отказа в обслуживании). +- **`code` — не аутентификатор**, а метка для выборки и визуальной сверки. + Он зашит внутрь подписанного payload, поэтому его нельзя вырвать из + подлинного запроса и подставить в поддельный. +- **Анти-фишинг:** если злоумышленник показывает на сайте чужой QR, код на + телефоне не совпадёт с кодом на экране браузера — заметно сразу. +- **Replay/одноразовость:** `code` живёт не дольше окна запроса (≤ 60 с); + сервис принимает каждый code один раз. +- **`msg` самозаявлена** и показывается только рядом с верифицированным + адресом; она нужна для узнавания, а не для доверия. + +## Совместимые SDK + +Готовые реализации чтения/отправки и серверной верификации: + +- **Android-кошелёк**: `:sdk` (niko_trust_android) — парсер QR + `sendLogin`. +- **Server/Go**: `pkg/protocol.VerifyApprovalRequest`, пример поллинга — + `examples/service`. +- **Rust**: клиентская библиотека `niko_trust_gui` (модули `protocol`/`relay`). diff --git a/docs/SERVICE-GUIDE.md b/docs/SERVICE-GUIDE.md index 8c25a01..66f589d 100644 --- a/docs/SERVICE-GUIDE.md +++ b/docs/SERVICE-GUIDE.md @@ -20,6 +20,14 @@ ключом пользователя и привязан к хешу запроса), а компрометация релея не даёт атакующему ничего, кроме отказа в обслуживании. +Есть два способа подтверждения: + +- **Адресный** (ниже): сервис уже знает `trust1...` пользователя и шлёт запрос + на его кошелёк. +- **QR / reverse** (без знания адреса): пользователь сканирует QR телефона и + сам отправляет подтверждение сервису. Подходит и для входа, и для + регистрации. Формат и правила — в [QR-LOGIN.md](QR-LOGIN.md). + ## Поток логина ```