Помоги разобраться #37
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Как устроена аутентификация устройства между UI и gateway?
@owner Привет! Разобралась, как устроена аутентификация устройства между UI и gateway. Ниже — полная картина со ссылками на код и документацию.
Коротко
Никаких cookie и токенов доступа. Аутентификация построена на device session + подписи каждого запроса ключом Ed25519:
device_session_id;Каноническое описание —
docs/ARCHITECTURE.md§15 (строки 574–805) иdocs/FUNCTIONAL.md§1 (строки 54–227).1. Регистрация устройства (вход по коду из письма) — REST
Маршрут неаутентифицированный (сессии ещё нет):
POST /api/v1/public/auth/send-email-code→ backend выдаётchallenge_idи отправляет код письмом.challenge_id+ код + свежий публичный ключ Ed25519 (base64, сырые 32 байта) + таймзону наPOST /api/v1/public/auth/confirm-email-code.device_session_id.Код gateway:
gateway/internal/restapi/public_auth.go(контрактAuthServiceClient.ConfirmEmailCode, полеClientPublicKey, строки 41–106). UI:ui/frontend/src/api/auth.ts, состояние и персистенция —ui/frontend/src/lib/session-store.svelte.ts. Приватный ключ хранится неэкспортируемым через WebCrypto (ui/frontend/src/platform/store/webcrypto-keystore.ts);device_session_idи ключ переживают перезагрузку (IndexedDB), потеря хранилища = повторный вход (ui/docs/auth-flow.md).2. Подпись каждого запроса (UI → gateway)
Транспорт авторизованного уровня — Connect / gRPC / gRPC-Web на одном h2c-порту. Конверт запроса (
gateway/proto/edge/v1/edge_gateway.proto:14-27):protocol_version="v1",device_session_id,message_type,timestamp_ms,request_id,payload_bytes,payload_hash(сырой SHA-256 от payload),signature.Подписываются не сами поля, а каноническая байтовая строка:
Кодирование: каждое строковое/байтовое поле —
uvarint(len)+ сырые байты;timestamp_ms— 8 байт big-endian; подпись — сырые 64 байта Ed25519. Реализация общая для клиента и сервера:ui/core/canon/request.go:53-73(компилируется в WASM) иgateway/authn/request.go:60-80. Их побайтовая идентичность закреплена тестамиgateway/authn/parity_with_ui_core_test.goиui/frontend/tests/wasm-core-canon-parity.test.ts.На стороне UI это делает
GalaxyClient.executeCommand(ui/frontend/src/api/galaxy-client.ts:76-115): строит канонические байты через WASM-ядро, подписывает их WebCrypto-ключом и отправляет конверт.3. Проверка на gateway
Цепочка middleware собирается в
gateway/internal/grpcapi/server.go:127-153, порядок (снаружи внутрь):protocol_version == "v1"—envelope.go.device_session_id—session_lookup.go. Резолв идёт через in-memory LRU-кэш (gateway/internal/session/), на промахе — синхронный REST к backend/api/v1/internal/sessions/{id}. Запись содержитUserID,ClientPublicKey,Status. Неизвестная или revoked сессия → отказ.SHA-256(payload_bytes)—payload_hash.go.signature.go→authn.VerifyRequestSignature(Ed25519 публичным ключом из сессии).freshness_replay.go:timestamp_msв пределах ±окна (симметрично, по умолчанию ±5 минут), затем резервирование(device_session_id, request_id)в Redis доtimestamp_ms + окно; повтор → отказ.rate_limit.go.message_typeвниз к backend; в заголовке прокидываетсяUserID, backend доверяет вердикту gateway и не выводит личность из тела (command_routing.go:70-92,docs/FUNCTIONAL.md§1.4).4. Обратное направление (gateway → UI)
Ответы и push-события подписывает gateway своим ключом Ed25519 (PKCS#8 PEM), а UI проверяет их доверенным публичным ключом (
VITE_GATEWAY_RESPONSE_PUBLIC_KEY): сверяет совпадениеrequest_id,payload_hashи подпись (galaxy-client.ts:117-148). Каноны ответа и события —"galaxy-response-v1" …и"galaxy-event-v1" …(ARCHITECTURE §15). Есть ещё неподписанный keepalivegateway.heartbeat, который UI игнорирует до проверки подписи.5. Отзыв сессии
Backend атомарно ставит
status=revoked, пишет аудит-строку и публикуетsession_invalidation. Gateway по этому событию помечает запись в кэше revoked и закрывает активные push-стримы; следующий lookup тоже вернёт revoked (TTL — страховка на случай потери события). UI: любое не-abort закрытие стрима трактуется как отзыв →signOut("revoked")(ui/frontend/src/lib/revocation-watcher.ts,docs/FUNCTIONAL.md§1.5).Маленькое наблюдение (не баг)
В
docs/ARCHITECTURE.md§15 «Verification order» (строки 762–763) подпись (шаг 4) указана перед проверкойpayload_hash(шаг 5), а в коде наоборот:payload_hash.goобёрнут снаружиsignature.go(server.go:131-132), то есть hash проверяется раньше подписи. На безопасность это не влияет (оба отказа происходят до обработки payload), но формально порядок в доке и в коде расходится. Если захочешь это поправить — заведи отдельную issue, и я приведу формулировку §15 в соответствие с кодом (это security-документ, править его в рамках вопроса не буду).Готова углубиться в любой из шагов, если нужно.
Working log (issue-agent) — click to expand
State: answered (owner satisfied; awaiting owner to close the issue)
Updated: 2026-05-24T00:05Z
Branch / PR: —
Confirmed: Answer-only
Kind/Questionissue (Russian): "how is device authentication structured between UI and gateway?". Full grounded answer already posted (comment 147) and accepted by the owner ("Понял, спасибо, закрывай issue").Open question: —
Decisions: Owner asked me to close the issue. Per absolute safety rule 8 I never create or close issues (owner-only — the owner's own deliberate setup). Replied (comment 156, Russian, warm voice, tagged author) explaining I cannot close it and asking the owner to close it themselves; kept
claude/blocked(owner's turn = close). No code change. Re-offered to aligndocs/ARCHITECTURE.md§15 verification-order with the code under a separate issue if wanted.Next: Owner to close the issue. If the owner instead asks a follow-up, answer again and re-block. Never close the issue myself.
Понял, спасибо, закрывай issue :)
@owner Рада, что картинка сложилась! :)
Вот только закрыть issue сама не могу — у меня это жёстко зашито: заводить и закрывать задачи может только ты, мне оставлены чтение, комментарии и метки. Это твоя же настройка (страховка, чтобы автономный агент ничего лишнего не прибил), так что тут я честно связана по рукам.
С моей стороны всё готово, так что закрой, пожалуйста, сам — это один клик. Метку
claude/blockedоставляю: ход за тобой.И на всякий случай напомню: если захочешь выровнять §15 в
docs/ARCHITECTURE.mdс кодом (тот мелкий нюанс с порядком проверкиpayload_hashи подписи) — заведи отдельную issue, и я аккуратно всё поправлю.developer referenced this issue2026-05-24 00:28:58 +00:00