Add authenticated protocol 2 and TypeScript WASM client
This commit is contained in:
1 parent
88ceeb2ae9
commit
237d404bcd
52 files changed
+3249
-150
No files matched your search
@@ -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).
|
||||
@@ -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.
|
||||
Reference in new issue
Block a user