Add authenticated protocol 2 and TypeScript WASM client

This commit is contained in:
sergeych committed 2026-10-19 04:17:34 +04:00
1 parent 88ceeb2ae9
commit 237d404bcd
52 files changed
+3249 -150

No files matched your search

+144
View File
@@ -0,0 +1,144 @@
# Оценка: полноценный Kiloparsec на TypeScript + WASM
Историческая оценка протокола 1, 18 октября 2026. После оценки библиотека перенесена
в модуль typescript основного репозитория Kiloparsec; протокол 2 исправляет найденные
KP-01/KP-02. Текущее состояние и регрессии: [SECURITY.md](SECURITY.md).
Числа ниже относятся к первому совместимому v1-варианту, не к новой v2-сборке.
Решение: **продолжать отдельную TS/WASM-библиотеку**.
Вариант light исключён. Kotlin/JS-обёртка оставлена как измеряемый эталон, а не
вторая продуктовая реализация. Сервер и исходный Kiloparsec не изменялись.
## Что сравнивали
Контрольные суммы использованных JVM-артефактов: [artifacts.json](../evaluation/artifacts.json).
Опубликованная Kotlin-библиотека поставляется как `.klib`, а не готовый TS API.
В исходниках нет публичного `@JsExport`-фасада. Поэтому сравнивали не размер klib,
а готовые минимизированные подключаемые пакеты:
- TS: все экспорты новой библиотеки, полный DH, подписи и шифрование, RPC,
локальные интерфейсы и сессии, кодеки, ключи, WebSocket-клиент;
- Kotlin: отдельная рабочая экспортируемая обёртка над опубликованным Kiloparsec,
блочный адаптер без намеренного включения Ktor WebSocket-клиента, DH, pinning,
идентификация и echo. Production DCE Kotlin, затем та же минимизация esbuild.
Оба пакета включают одну версию libsodium-wrappers-sumo 0.7.15 и её WASM payload.
Сравнение благоприятно для Kotlin: его фасад существенно уже по API, чем TS.
Это не сравнение полного равенства возможностей или двух готовых релизов.
| Измерение | TS/WASM | Kotlin/JS baseline |
|---|---:|---:|
| Минимизированный пакет | 1 124 612 байт | 1 982 755 байт |
| gzip, уровень 9 | 330 101 байт | 559 003 байта |
| Инициализация + создание Ed25519, медиана 5 процессов | 88,59 мс | 166,50 мс |
| RSS процесса после инициализации, медиана | 62,9 МБ | 70,6 МБ |
| Прирост RSS, медиана | 19,3 МБ | 27,0 МБ |
Выигрыш размера около **43%**, по gzip — **41%**. Время/RSS — локальное наблюдение
на одном хосте, не SLA; время не включает запуск Node. Числа воспроизводятся через
`scripts/measure.mjs`, исходные измерения сохранены в
[measurement record](../evaluation/measurements.json). Готовые измеряемые пакеты
проверены загрузкой; Kotlin bundle выполнил реальный authenticated echo.
Большая часть TS-пакета — готовая криптобиблиотека; специальная урезанная сборка
sodium здесь не создавалась и в выигрыш не заложена.
## Реально выполнено
TS-клиент работает с опубликованным JVM Kiloparsec 0.7.2-SNAPSHOT без его правок:
DH, проверка закреплённого серверного ключа, идентификация клиента, строки/числа/
списки, callback с вложенным обратным RPC, push, удалённые ошибки, 64 параллельных
вызова, большой строковый аргумент, ping и новое подключение после закрытия.
Для воспроизведения `npm run test:interop` запускает собственный локальный сервер.
Проверены unit-тесты кодирования, криптографические векторы, таймаут/abort/close,
полный дуплекс, pinning, подмена/истечение подписи, повреждение/повтор шифротекста.
Один тест намеренно воспроизводит обнаруженный изъян корреляции ответа.
Ни тест производительности реального сервиса, ни аудит всей библиотеки, ни
браузерные runtime-тесты этим не подменяются.
## Что выяснилось при переносе
1. Kotlin bridge требует корректного порядка загрузки sodium: импорт Kotlin до
sodium.ready ломает опубликованный binding. Обёртка должна это скрывать.
2. Неаннотированный UInt кодируется по-разному в опубликованном BiPack 0.3.2 и
более новом исходнике 0.4.0. Перенос проверен именно с опубликованными артефактами;
версии/контрольные суммы нужно закреплять, а не ориентироваться только на master.
3. Исходный signed Smartint не сохраняет Long.MIN_VALUE. TS явно отвергает это
значение, чтобы не выдавать тихо повреждённые данные.
4. **Существующий протокол позволяет переставлять ответы между RPC**, изменив
только внешние forId. Изъян воспроизведён. Есть также неподписанное подтверждение
клиентской идентификации. Подробности и предложения — [SECURITY.md](SECURITY.md).
## Граница результата и следующее решение
Оценка закончена, путь выбран, рабочий совместимый клиент реализован отдельно в
`~/dev/kiloparsec/typescript`. Это кандидат библиотеки, не готовая поставка в биржу.
Первоначальный совместимый перенос не требовал изменения сервера, но унаследовал
KP-01/KP-02. По последующему решению пользователя обе стороны переведены на
протокол 2: ID/тип/приоритет аутентифицированы, завершение идентификации зашифровано.
Старая версия автоматически не выбирается. Технический разбор и доказательства
исправления находятся в SECURITY.md. Изменение сервера здесь относится к исходникам
библиотеки и локальному тестовому стенду; развёртывания сервисов не выполнялись.
Нет автоматического reconnect/повтора бизнес-команд, TCP/UDP/carrier cycling,
генератора всех Kotlin DTO или квалификации браузеров. Приоритет пакета передаётся
совместимо, но первый TS-клиент обслуживает выход FIFO. Эти ограничения явно
перечислены в README; они не скрыты обещанием полного совпадения API Kotlin.
Ни сервис GrandMobile, ни SDK Gold, ни сайт биржи этой работой не развёрнуты и
не переключены на новый транспорт. Модуль сопровождается вместе с Kotlin в одном репозитории; публикация не выполнялась.
## Повторный замер исправленного протокола 2
Текущий модуль 0.8.0: TS — 1 126 081 байт, gzip 330 527 байт; Kotlin/JS-обёртка
над текущими исходниками — 1 993 070 байт, gzip 561 965 байт. Выигрыш сохраняется:
примерно 43,5% по размеру и 41,2% по gzip. Медиана инициализации с генерацией
Ed25519: 87,0 мс против 169,6 мс на этом хосте. Полные исходные данные:
[measurements-v2.json](../evaluation/measurements-v2.json). Старые данные выше
сохранены как основание исходного выбора, а не выданы за текущий релиз.
## Сравнение с Kotlin/Wasm, протокол 2
Проверили также **настоящую оптимизированную Kotlin/Wasm-сборку**, а не `.klib`.
Модуль `evaluation/wasm-baseline` использует тот же исходник Kiloparsec через
Gradle composite build. Минимальная обёртка сохраняет DH, проверку серверного
ключа, идентификацию клиента и echo RPC — как Kotlin/JS baseline выше.
| Поставка | Размер, байт | gzip level 9, байт | Инициализация + Ed25519, медиана |
|---|---:|---:|---:|
| TS + sodium WASM | 1 126 081 | 330 527 | 87,0 мс |
| Kotlin/JS + sodium WASM | 1 993 070 | 561 965 | 169,6 мс |
| Kotlin/Wasm + JS loader + sodium WASM | 2 083 682 | 616 011 | 116,1 мс |
По сравнению с Kotlin/Wasm TS меньше на **46,0% по исходному размеру** и на
**46,3% после gzip**. По сравнению с Kotlin/JS выигрыш составляет 43,5% и 41,2%.
В Kotlin/Wasm посчитаны два реально необходимых файла: оптимизированный Binaryen
исполняемый `.wasm` — 764 584 байта (gzip 247 073), минимизированный ESM loader
с зависимостями sodium/joda — 1 319 098 байт (gzip 368 938). Gzip применяется
**к каждому файлу отдельно**, после чего размеры складываются. Sodium WASM уже
встроен в loader и учитывается один раз; обе crypto-привязки используют один
CommonJS-модуль sodium. Карты исходников, неоптимизированный `.wasm`, `.klib`,
тестовый runtime и компилятор в поставку не включены.
Получившаяся поставка успешно загрузилась на Node 24.19.0, создала Ed25519-ключ,
выполнила DH, проверку закреплённого серверного ключа и authenticated echo с
локальным JVM-сервером протокола 2. Временный сервер после проверки остановлен.
Браузерные runtime-проверки этим не подменяются. Время/RSS остаются наблюдением
на одном Linux x64 хосте, а не обещанием производительности на других платформах.
[Переносимый протокол измерения](../evaluation/wasm-measurements.json) содержит
состав поставки, относительные пути, SHA-256 каждого файла и пять исходных замеров.
Для воспроизведения из `typescript/`:
```sh
npm run wasm:build
npm ci --prefix evaluation --ignore-scripts
npm run measure:wasm
npm run fixture:build
npm run test:wasm-baseline
```
Инструкции и ограничения упаковки приведены в
[evaluation/README.md](../evaluation/README.md#optimized-kotlinwasm-comparison).
+93
View File
@@ -0,0 +1,93 @@
# Authenticated RPC and handshake: protocol 2
Protocol 2, implemented with library version 0.8.0, fixes two weaknesses found
while building the TypeScript client. Both Kotlin and TypeScript use this profile
by default and reject protocol 1. Deploy compatible peers together; there is no
automatic fallback to the vulnerable envelope.
## KP-01 — outer RPC response IDs were unauthenticated
The old Kotlin `KiloL0Interface` encrypted a result whose inner `forId` was always
zero. The actual pending RPC ID was in an unencrypted outer Transport.Response.
An active intermediary could exchange two outer IDs without modifying either
ciphertext. With compatible result codecs, request A received B's valid answer.
This was reproduced before the fix; passive decryption was not required.
Protocol 2 seals this record inside the existing session encryption:
```
ProtectedMessage(kind, correlation, priority, payload)
kind = call | push | response | error | server_finished
```
A call's correlation is its actual nonzero outer Transport ID; a push has zero.
Before application lookup/deserialization, the receiver validates the protected
kind, ID and priority against the outer dispatch metadata. Responses/errors echo
the authenticated request ID and priority. The caller checks these before decoding
or delivering a result. The inner response/error Block remains forId=0, but no
longer supplies the request correlation. Rewriting a response's outer forId now
closes the connection instead of resolving the wrong call. Rewriting call/push or
priority likewise fails before application dispatch. An unencrypted outer error
is not accepted as an authenticated application result.
Session nonces are replay-checked on both peers, including pushes. A bounded
4096-position window supports responses completed out of order and rejects replay
or excessively stale traffic. Send nonces and transport IDs do not wrap. Nonce
state used during identity exchange is preserved when the identity is completed.
The routing envelope still allows denial of service by an active carrier: packets
can be dropped, delayed or redirected to unknown IDs. Channel encryption cannot
force an adversarial carrier to deliver data. Public outer priority may influence
transport scheduling before the receiver authenticates it; it cannot authorize
an altered application call.
## KP-02 — identity acknowledgement was plaintext
The old `L0ClientId` returned a plaintext Unit. A relay could forge successful
completion without the server accepting the client's identity.
The v2 reply is an encrypted `server_finished` frame with correlation/priority zero
and payload equal to the exact ClientIdentity plaintext the server accepted.
The client validates all of these before returning a connected client or invoking
its connected handlers. A plaintext Unit or an altered identity is rejected.
Server-side per-operation authorization remains mandatory.
Server and client proofs use different signing domains:
```
UTF8("kiloparsec/v2/server\0") || sessionToken
UTF8("kiloparsec/v2/client\0") || sessionToken
```
Here `\0` denotes one NUL byte, not a backslash and zero on the wire. This binds
the proof to the role and version. A legacy signature of the token alone is invalid.
The handshake version is exactly 2 at both peers; version changes are rejected.
The server also rejects a repeated handshake/identity exchange within a connection
and creates a fresh exchange keypair per connection.
## Identity trust
The TypeScript client requires a preconfigured Ed25519 server key and validates its
Seal before signing the client proof. Kotlin retains its existing application
policy for optional remote identity: a valid Seal establishes possession of its
key, not external trust in that key. Kotlin applications must check the expected
remoteIdentity as part of their connection policy; anonymous sessions should be
used only where explicitly authorized. Version 2 fixes RPC binding; it does not
silently invent a key provisioning or trust-on-first-use policy.
## Evidence
- `test/security.test.mjs`: swapped responses reject; wrong pin, version/role/token
signature substitution, forged/mismatched finished, expired/tampered server Seal,
damaged/replayed packets, altered call IDs/kinds/priorities fail closed.
- Kotlin `AuthenticatedEnvelopeTest`: normal signed duplex and session identities,
response-ID swap, call-to-push rewrite, priority rewrite, replay, forged Unit
acknowledgement and protocol downgrade.
- The interoperability fixture consumes this checkout, rather than an old Maven
snapshot, and runs TS ↔ JVM full-duplex calls, errors, pushes, concurrency and
reconnect to a fresh session.
These regressions exercise the identified flaws. They are not a general independent
cryptographic audit. Browser runtime and sustained load remain separate release
qualification work. Secret export is explicit; wiping library-owned byte buffers
does not promise erasure of copies made by a host VM or device.