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
This commit is contained in:
parent
3bf13fa488
commit
1b6a1984c0
2 changed files with 123 additions and 0 deletions
115
docs/QR-LOGIN.md
Normal file
115
docs/QR-LOGIN.md
Normal file
|
|
@ -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": "<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`).
|
||||
|
|
@ -20,6 +20,14 @@
|
|||
ключом пользователя и привязан к хешу запроса), а компрометация релея не даёт
|
||||
атакующему ничего, кроме отказа в обслуживании.
|
||||
|
||||
Есть два способа подтверждения:
|
||||
|
||||
- **Адресный** (ниже): сервис уже знает `trust1...` пользователя и шлёт запрос
|
||||
на его кошелёк.
|
||||
- **QR / reverse** (без знания адреса): пользователь сканирует QR телефона и
|
||||
сам отправляет подтверждение сервису. Подходит и для входа, и для
|
||||
регистрации. Формат и правила — в [QR-LOGIN.md](QR-LOGIN.md).
|
||||
|
||||
## Поток логина
|
||||
|
||||
```
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue