niko_trust/docs/QR-LOGIN.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

8.6 KiB
Raw Permalink Blame History

QR-LOGIN — Код-подтверждение входа по QR

Стандарт на «reverse»-подтверждение: телефон сканирует QR и сам отправляет подтверждение сервису, вместо того чтобы сервис заранее знал адрес пользователя. Работает и для входа, и для регистрации, и не требует изменений в протоколе TCE — используется обычный объект ApprovalRequest.

Этот документ задаёт единый формат QR и правила взаимодействия: его обязаны понимать (а) клиент-кошелёк (Android), который печатает QR на камеру и (б) сервис, который QR показывает.

Формат QR

QR кодирует компактный 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": "<code из QR>"} (код попадает внутрь подписи);
    • 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).