- 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
8.6 KiB
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 от пользователя является
подтверждением личности. Сервису не нужно заранее знать адрес — он получает его
из проверенной подписи отправителя.
Правила для клиента (кошелька)
- Валидация QR:
v == 1,type == "nt-login",relay— валидный URL,svc— валидный адресtrust1...(каноническая проверка public key),code— ровно 6 цифр,msg, если есть, ≤ 64 символов. Любое нарушение — отказать с объяснением. - Диалог подтверждения до отправки:
- заголовок =
msg(если есть, иначе «Вход в сервис»); - обязательно показать короткий адрес сервиса рядом с
msg— имя самозаявлено, ему не доверяют (см. INV-7, алиасы неавторитетны); - крупно показать
codeдля визуальной сверки с экраном браузера.
- заголовок =
- Отправить один
ApprovalRequest:sender= ключ пользователя (подпись ставит клиент);recipient=svc;action="login";payload={"code": "<code из QR>"}(код попадает внутрь подписи);created_at= now,expires_at= now + ≤60 (ограничение формата TCE);- записать в историю.
- Не показывать
codeв истории постоянно; пометить статусом «отправлено».
Правила для сервиса
- Сгенерировать одноразовый
codeи показывать QR вместе с ним. - Опрашивать
/v1/requests?recipient=<свой адрес>&after=<курсор>(или подписаться на каналrequestsпо WS) со своей сессией чтения. - Для каждого конверта:
VerifyApprovalRequest→ проверить свежесть окна (в границахcreated_at..expires_atза 60 с) → сопоставитьpayload.codeсо своим активным QR. - Совпадение по
code+ прошедшая подпись ⇒ личность =req.sender. Выпустить собственную сессию для этого адреса. - (Опционально) закрыть петлю: подписать
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).