Files
kiloparsec/CONCEALED-PROTOCOL-WIRE.md

310 lines
29 KiB
Markdown

# Concealed Kiloparsec: experimental wire specification
Draft 0, 2026-10-22. **Experimental source implementation exists; not independently audited or production-qualified.** See [implementation and validation](CONCEALED-PROTOCOL-IMPLEMENTATION.md). This document turns the agreements in [CONCEALED-PROTOCOL-DESIGN.md](CONCEALED-PROTOCOL-DESIGN.md) into a concrete candidate suite for Q2. Design references D1–D14 and Q1–Q14 retain their original meanings. A changed byte layout, primitive, or derivation requires a new suite; ordinary tuning must not silently change the wire format.
## 1. Scope and notation
The suite identifier is `1`, internal to authenticated encryption. This is a concealed connection flavor, not a renaming of released protocol 2. Endpoints explicitly select this flavor; no autodetection, downgrade, or TypeScript client change. TCP and WebSocket use stream records; UDP uses independent records. WebSocket message boundaries carry no additional protocol meaning: concatenate binary message payloads into the ordered byte stream. Reject text messages.
Integers are unsigned, fixed-width big-endian, except curve encodings, which follow their primitive's standard little-endian representation. `u8`, `u16`, `u32`, `u64` denote encoded widths. `a || b` denotes exact byte concatenation. `V(x) = u32(length(x)) || x`. No implicit text normalization, serializer metadata, native integer encoding, or trailing bytes are allowed. Text constants use ASCII. A label is `V(ASCII(label))`.
`H(x)` is BLAKE2b with output length **32**, no key. `B(n,k,x)` is BLAKE2b with configured output length `n` and key `k`; it is not truncation of a differently configured digest. Define:
```
Derive(n, secret, label, context) =
B(n, secret, V("kiloparsec/concealed/1") || V(label) || V(context))
```
Every input called `secret` below is exactly 32 bytes, except the explicitly 64-byte ordered directional-key material in token derivation. Keys never cross roles. Context and complete transcript hashes are public bindings, not entropy sources. Constant-time comparison is required for tags and secret-derived proof values. All randomness comes from libsodium's CSPRNG.
## 2. Suite and provisioned credentials
| Purpose | Primitive |
| --- | --- |
| Bootstrap agreement | X25519 with Elligator 2 concealed ephemeral public representation |
| Session agreement | libsodium `crypto_kx` (X25519/BLAKE2b), separate fresh key pairs |
| Bootstrap encryption and UDP | XSalsa20-Poly1305 `crypto_secretbox_easy` |
| Continuous stream encryption | Original ChaCha20, 32-byte key, 8-byte nonce, 64-bit block counter |
| Stream object authentication | Keyed BLAKE2b with 16-byte output |
| Hash/KDF | BLAKE2b as defined above |
| Identity signature | Ed25519 detached signature, 32-byte public key, 64-byte signature |
This first suite supports Ed25519 identity keys only. Other crypto2 key formats need another authenticated suite/identity encoding; do not silently serialize arbitrary polymorphic key objects here.
Client configuration contains an authentically obtained server static X25519 encryption public key `E` and trusted server Ed25519 verifying key `Vs`, with their association governed by application policy. The server owns corresponding private keys. No wire key identifier or automatic rotation protocol is present. Distinct configured endpoints can serve distinct credential sets. Future support for multiple retained keys must impose a small configured trial bound.
Original ChaCha20 is chosen because the cached common libsodium binding exposes it, including the 64-bit `initialCounter`; its XChaCha20 stream functions are commented out for Apple. This is a concrete candidate primitive under D11, not a claim that the complete composition has received independent review.
## 3. Elligator interoperability profile: qualification prerequisite
Let `b` be a fresh clamped X25519 scalar. Generate a Curve25519 public point including a uniformly randomized small-order component before inverse Elligator mapping. Retry with fresh local material if no representative exists. Encode the successful representative `R` as exactly 32 bytes with the otherwise unused high-bit choices randomized. Decode `R` to Montgomery public coordinate `P`. The server's static scalar is clamped, so its multiplication removes the small-order component. Preserve the secret scalar associated with `P` until bootstrap completion.
The Kotlin prototype now implements the [elligator.org Curve25519 profile](https://elligator.org/map), with `p = 2^255-19`, `A = 486662`, `Z = 2`. Square roots select the integer representative in `[0,(p-1)/2]`. Two most-significant serialized bits are independent random padding and are masked on decode. A separate random bit selects the inverse-map branch corresponding to the point's v sign. The full-curve public point is `clamp(b)*G + (b mod 8)*H`; the order-eight Edwards generator H has y coordinate `2707385501144840649318225287225658788936804267575313519463743609750303402022` and even x. Convert Edwards y to Montgomery `u=(1+y)/(1-y)` modulo p.
Published direct, inverse, and special scalar-multiplication vectors from [elligator.org/vectors](https://elligator.org/vectors/) are checked into `ElligatorReferenceVectors.kt` (retrieved 2026-10-22; initial fixture SHA-256 `d4518186b46e58476c2bfd5a3a6cd36f413cd8d969384d7cc59b4843651d85a3`). Tests cover all eight torsion components and independent libsodium agreement. This pins the prototype's profile; it does not establish all implementation properties. Expand to the extended vector/randomized corpus before qualification. Do not claim byte-for-byte compatibility with a particular Monocypher release without checking it separately.
Every use of a received X25519 public input checks its exact 32-byte width, propagates native rejection, and rejects an all-zero shared result before KDF. Do not regenerate local keys in response to a rejected peer key. The bignum mapping is variable-time, but the current call graph supplies public coordinates rather than the server static scalar. See the [timing assessment](CONCEALED-PROTOCOL-TIMING.md) for measurements, dataflow limits and response-shaping experiments; no constant-time or general timing-concealment guarantee is made.
## 4. Common protected bootstrap envelope
The first two flights use separately authenticated hidden lengths. This avoids a visible length field and avoids general object parsing before authentication.
Given a 32-byte root `K`, a context `C`, and a payload `M`:
```
Kh = Derive(32, K, "flight/header-key", C)
Kb = Derive(32, K, "flight/body-key", C)
Nh = Derive(24, K, "flight/header-iv", C)
Nb = Derive(24, K, "flight/body-iv", C)
L = u32(length(M))
Header = SecretBoxEasy(L, Nh, Kh) // 16-byte tag || 4 ciphertext bytes
Body = SecretBoxEasy(L || M, Nb, Kb) // 16-byte tag || ciphertext
Envelope(K,C,M) = Header || Body
```
`SecretBoxEasy` has exactly the libsodium combined layout. Distinct keys and IV derivations separate header/body operations. Duplicating `L` inside the body binds header and body; compare exact copies after both authenticate. Authenticate the 20-byte header before allocating the bounded body. Reject over-limit lengths, invalid lengths, failed authentication, and extra bytes within a datagram. Envelope payload length includes random padding. A stream reads precisely `20 + 16 + 4 + length(M)` bytes. The maximum flight includes its exposed prefix and all envelope overhead.
For UDP an envelope occupies one datagram. While waiting for a flight, silently discard undersized/oversized or unauthenticated datagrams and continue within the original attempt deadline. In particular, a valid authenticated header followed by a corrupted body must not consume the pending handshake state. Only a completely authenticated envelope with invalid semantic fields/signature makes the attempt terminal. TCP authentication failure remains terminal because there is no safe stream resynchronization. No preliminary error response, parsing oracle, or unauthenticated error record is sent. Invalid unauthenticated input is silently dropped; TCP may close without sending bytes. Enforce admission budgets before expensive key agreement, and whole-attempt/read deadlines throughout.
## 5. Client HELO, flight 1
Generate separate fresh bootstrap scalar `b` and fresh session `crypto_kx` pair `(c,C)`. They must not be reused across attempts, and must not be the identity signing key. `C` appears only inside the encrypted HELO.
```
Zb = X25519(b, E) // server computes X25519(e, P)
U = Random[24]
BootContext = R || U || E
Kboot = Derive(32, Zb, "bootstrap", BootContext)
HeloCore =
u8(1) // suite
|| u8(carrier) // 1 = stream, 2 = UDP
|| C[32]
|| Vs[32] // intended server identity
|| u32(receiveBodyLimit)
|| u16(receiveDatagramLimit) // 0 for stream; includes full datagram
|| u32(paddingMaximum)
HeloPayload = u32(length(HeloCore)) || HeloCore || randomPadding
F1 = R[32] || U[24] || Envelope(Kboot, H(BootContext), HeloPayload)
h1 = H(V("helo") || V(F1))
```
HeloCore is exactly **76 bytes**; F1 is **176 bytes plus padding**. All fields are fixed for suite 1; reject an unknown suite/carrier or a carrier inconsistent with this endpoint. No feature bitmask with undefined semantics is accepted. A server compares the intended `Vs` to its configured identity. Local limits constrain parsing before any peer-advertised limit. The inner core length must equal the suite-1 core size; the remaining bytes are padding.
Bind the exact representative bytes into the KDF, including bits ignored by the map. Equivalent representatives must not let an attacker mutate an accepted flight. The optional recent-client-session-DH ring guard atomically checks/inserts `C` after successful decryption and validation; duplicates are silently ignored. The guard is operational only and need not persist across restart.
## 6. Server hello, flight 2
After accepting HELO, generate fresh session `crypto_kx` pair `(s,S)` and random reply IV `I[24]`. The client session public key `C` is confidential bootstrap material here. Domain-separated derivation from it is the Q1 decision; it is not a general-purpose KDF from public information.
```
Kreply = Derive(32, C, "server-reply", h1 || E || Vs)
ReplyContext = H(V("server-flight") || h1 || I)
Proof = H(V("helo-decrypted") || C)
ServerCore =
u8(1) || u8(carrier)
|| S[32] || Proof[32] || Vs[32]
|| u32(serverReceiveBodyLimit)
|| u16(serverReceiveDatagramLimit)
|| u32(serverPaddingMaximum)
ServerSignature = Ed25519Sign(serverSigningKey,
V("kiloparsec/concealed/1/server-auth") || h1 || V(ServerCore))
ServerPayload = u32(length(ServerCore)) || ServerCore
|| ServerSignature[64] || randomPadding
F2 = I[24] || Envelope(Kreply, ReplyContext, ServerPayload)
h2 = H(V("handshake") || V(F1) || V(F2))
```
ServerCore is exactly **108 bytes**; F2 is **240 bytes plus padding**. The client authenticates hidden length/body, verifies suite/carrier, proof, intended server key, and signature before completing session derivation. The explicit proof is retained as agreed although authenticated response decryption already supplies a related proof. The server signature covers selected parameters, both session keys indirectly (`C` through exact F1), intended static identity through F1, and the response role.
Effective outbound bounds are the smaller relevant local/peer receive bounds. Peer claims can reduce a sender's allowance; they cannot increase a local allocation cap. The server may reduce its own padding; it must not require padding or silently change algorithms. UDP F2 must fit both configured datagram budgets and pre-validation amplification allowance; pad F1 sufficiently or reject locally before sending an unsupported attempt. Before valid ACK, server bytes sent must not exceed three times bytes received for the attempt.
A new reception of an old HELO after guard eviction generates fresh S and I. Random I prevents reply key/IV reuse. It does not turn the old hello into an authenticated client.
## 7. Session material, token, and handshake keys
Use the ordinary fresh session pairs through libsodium `crypto_kx`:
```
C2S = client.tx = server.rx // 32 bytes
S2C = client.rx = server.tx // 32 bytes
Token = Derive(32, C2S || S2C, "connection-token", h2)
```
Ordered directional key material, not process-local sending/receiving order, is essential. Do not substitute crypto2 `SessionKey.sessionTag` without checking its exact derivation against these bytes.
For each direction `d` in ASCII `c2s`, `s2c`, let `Kd` be the corresponding crypto_kx output. Define:
```
Kfinish[d] = Derive(32, Kd, "finished/" || d, h2)
```
ACK and acceptance use independent secretbox records even on TCP. They do not consume the subsequent continuous stream's cipher position. This makes handshake retransmission independent of stream state and avoids changing application traffic keys before both identities are bound.
## 8. Client ACK, flight 3
After server authentication, the client signs the session binding with its identity key. This suite supports an Ed25519 client identity `Vc`; application policy decides whether it is authorized. Optional anonymous identity requires a future explicit policy/specification; it is not an absent signature.
```
ClientSignature = Ed25519Sign(clientSigningKey,
V("kiloparsec/concealed/1/client-auth") || Token || h2 || Vc)
AckCore = u8(3) || h2[32] || Vc[32] || ClientSignature[64]
AckPayload = u32(length(AckCore)) || AckCore || randomPadding
A = Random[24]
F3 = A || Envelope(Kfinish[c2s], H(V("ack") || h2 || A), AckPayload)
h3 = H(V("authenticated-handshake") || h2 || V(F3))
```
AckCore is exactly **129 bytes**; F3 is **197 bytes plus padding**. A server accepts F3 only for the pending attempt associated with its listener and, for UDP, source address/port. It checks exact core size, role, transcript, signature, and authorization policy. No RPC is dispatched based on an unverified identity. No connected callback or authorization hook is repeated for a retransmitted identical authenticated F3.
F3 is retransmitted **byte-for-byte**, with the same A and ciphertext. Never regenerate a signature/padding/IV for a retry of this flight. Reusing an identical cached ciphertext is not encrypting a different plaintext with reused nonce material.
## 9. Server acceptance, flight 4
After successful client identity verification and application authorization:
```
AcceptCore = u8(4) || h3[32]
AcceptPayload = u32(length(AcceptCore)) || AcceptCore || randomPadding
J = Random[24]
F4 = J || Envelope(Kfinish[s2c], H(V("accept") || h2 || J), AcceptPayload)
```
AcceptCore is exactly **33 bytes**; F4 is **101 bytes plus padding**. There is no plaintext rejection variant. Failure aborts the attempt; authenticated diagnostic design is outside this initial suite. The client verifies F4 and its h3 before reporting connected. Cache exact F4 for the bounded UDP retry interval. Only a matching authenticated exact F3 may trigger its resend, within retry/rate budgets.
Identity-ready, authorized dispatch, and public connected state are separate lifecycle events internally. Existing onConnected policy handlers are synchronous KiloScope receiver functions, not suspending RPC callbacks. They inspect verified credentials before acceptance/readiness. An RPC launched asynchronously by a handler waits on the existing deferred readiness gate and runs after setup; a handler must not block waiting for it. Server policies run before F4; client policies run after F4 verification and before local readiness/connected notification. Preserve this existing initialization contract (D13/Q5), and test that background-launched calls release after readiness while rejecting policies fail queued calls. The handshake dispatcher remains separate from RPC.
## 10. Application traffic keys
Bind the client identity and exact ACK into traffic keys, avoiding unknown-peer associations across an identical DH transcript:
```
For each d:
KstreamBase[d] = Derive(32, Kd, "traffic/stream/" || d, h3)
StreamSalt[d] = Derive(24, Kd, "traffic/salt/" || d, h3)
Kmac[d] = Derive(32, Kd, "traffic/mac/" || d, h3)
Kudp[d] = Derive(32, Kd, "traffic/udp/" || d, h3)
```
The record engine expands the stream base/salt using exact ASCII labels (without V prefixes):
```
Kstream[d] = B(32, KstreamBase[d],
ASCII("kiloparsec/concealed/stream-key/v1") || StreamSalt[d])
Nstream[d] = first8(B(16, KstreamBase[d],
ASCII("kiloparsec/concealed/stream-nonce/v1") || StreamSalt[d]))
```
This two-stage derivation is the record engine's reusable API boundary; handshake-derived inputs bind direction and h3. BLAKE2b generic-hash output lengths are 16–64, so the 8-byte nonce is explicitly truncated from the 16-byte configured digest. h3 is available to both peers before acceptance; F4 confirms its agreement. Neither F4 randomness nor retransmission changes traffic keys.
Erase raw bootstrap/shared/finished material when it is no longer needed, subject to bounded retransmission state. JVM garbage collection limits guarantees of physical erasure; document actual platform behavior rather than promising complete erasure.
## 11. Protected record body
A record's authenticated body is:
```
u32(objectLength) || objectBytes || randomPadding
```
The first byte of objectBytes is the message kind: `0 = RPC`, `1 = CLOSE`. For RPC the remaining bytes are one existing BiPack-encoded `Transport.Block` with no existing L0Call encryption wrapper. Decode only the exact objectLength bytes after integrity verification; require complete consumption. CLOSE objectBytes is exactly `u8(1) || u16(reason)` with reason `0 = normal`, `1 = local shutdown`, `2 = usage limit`; reject undefined reasons in suite 1. An authenticated CLOSE conveys no success for pending RPCs.
Direction and transcript are bound through separately derived MAC keys; they need not be repeated in each MAC input. The authenticated body delimiter fixes the boundary between object and padding. Padding byte content is random, with an independent uniformly sampled length between zero and configured effective maximum. UDP reduces padding before rejecting an oversized unpadded record. No filler is appended outside authentication/encryption.
## 12. Stream application records
Maintain independent byte cursor and implicit record sequence q per direction, both starting at zero after F4. For body B:
```
L = u32(length(B))
T = B(16, Kmac[d], ASCII("kiloparsec/concealed/record-mac/v1")
|| u64(q) || L || B)
PlainRecord = L || B || T
WireRecord = PlainRecord XOR nextChaCha20Bytes(length(PlainRecord))
```
ChaCha20 begins at block counter zero with Kstream/Nstream. For byte cursor p, the relevant block is `floor(p/64)` and within-block offset is `p mod 64`. Socket segmentation must not reset the cursor or discard unused block bytes. A binding call with `initialCounter` starts at a block boundary: retain leftovers or generate/discard the prefix of that block correctly. Never restart the primitive per object with counter zero.
Decrypt exactly 4 header bytes; enforce local/effective body limit before allocation; read body/tag with partial-record deadline; recompute MAC using exact bytes and constant-time compare; only then parse the body and BiPack object. Increment q after successful authentication. Any bad tag, malformed authenticated record, over-limit length, partial-record timeout, or premature EOF fails the channel. Never scan for resynchronization.
A single writer selects encoded plaintext objects from OutQueue using admission ordinal and priority, assigns q in selected order, protects, and writes each record completely. Enqueue admission is atomic. Cancellation after stream mutation must finish that record or abort; it cannot roll back a cipher cursor. The existing queue's 2730-position bound supplies fairness; no null-ordinal normal entries bypass it. A large object is not preemptible after writing starts.
EOF following authenticated CLOSE is clean; EOF without CLOSE is abrupt/truncated. CLOSE follows drained admitted writes, with the configured drain deadline. No half-close API initially.
## 13. UDP application records
Each direction has a protected u64 sequence q starting at zero. Allocate sequence, encrypt, and enqueue atomically under the preparation mutex; await transmission outside it.
```
I = Random[24]
Plain = u64(q) || Body
Datagram = I || SecretBoxEasy(Plain, I, Kudp[d])
```
Overhead before object bytes/padding is 24 + 16 + 8 + 4 = **52 bytes**, including an object kind byte inside objectBytes makes RPC framing overhead 53 bytes before BiPack bytes. The encrypted inner delimiter and sequence are authenticated. Require the complete datagram within configured/negotiated budget. Verify secretbox before touching replay state or interpreting record fields. Reject malformed valid records; silently discard unauthenticated garbage without closing an established UDP session. Duplicate/stale authenticated data records are nonfatal discards. Replay tracking uses the existing 4096-position window; local overtaking is at most 2730.
CLOSE is sent once, best effort. Lost/reordered datagrams do not imply delivery of prior RPCs. No new application retransmission, fragmentation/reassembly, NAT rebinding, or session migration is provided. Keep one active authenticated association per source/listener under existing adapter ownership rules; fresh unverified HELOs cannot replace it.
Handshake retries remain separate from normal replay filtering. Pending state knows which flight it expects; acceptance cache recognizes exact authenticated F3 without rerunning callbacks. After establishment, process a possible retry as such only after verifying its matching finished key/context; do not reinterpret arbitrary application datagrams as RPC or mutate a live session based on unauthenticated classification. The absence of a visible session selector requires a strictly bounded number of candidate states per tuple. Initial implementation may permit one pending attempt and one established/retry state per tuple; retire superseded pending attempts without replacing the established one.
## 14. Limits and failures
Use the grouped immutable options and defaults from design section 6. Fixed security properties are not optional: authentication, trusted identity verification, transcript binding, failed-DH rejection, 16-byte tags, 24-byte independent-record IVs, and non-wrapping counters.
For this candidate suite, proactively reconnect before a direction exceeds **2^32 application records** or, for streams, **2^40 encrypted plaintext bytes** (including encrypted length and MAC). Datagram records are bounded to 65507 bytes each, so the 2^32-record ceiling implies fewer than 2^48 encrypted bytes; there is no additional cumulative UDP byte counter. Handshake keys are flight-specific and bounded by flight size/retry budgets. These are conservative selected engineering ceilings, not a security-bound proof. Check projected use before admitting a record; never wrap q, cursor, or cipher counter. Usage-limit CLOSE may use a reserved final allowance; otherwise abort. Do not reset counters under existing keys. No in-place rekey protocol in suite 1.
Bootstrap defaults: 15-second whole-attempt deadline, three fresh attempts, 16-KiB stream-flight cap, UDP cap 1200 bytes. F1/F2/F3/F4 must each fit its carrier cap after overhead. Every inner length is validated against its authenticated container. Stream body default 16 MiB, minimum body size 4 (a control/object decoder may impose a larger minimum); partial-record deadline 30 seconds beginning with the first byte, not while idle. Queue 128 objects/32 MiB, padding default 0–255, graceful close 2 seconds. Admission budgets, retransmission and cache bounds follow Q10/design section 6.
A local, server-only failure-work policy defaults to one discarded HELO decryption after envelope authentication fails, using a mutated copy of an independent throwaway scalar. It shares crypto admission bounds, never changes the real outcome or emits bytes, and can be disabled by `Handshake.decoyOnHelloAuthenticationFailure`. It changes no wire field and is not an additional authentication check. See the [timing assessment](CONCEALED-PROTOCOL-TIMING.md#7-requested-server-side-decoy-decryption).
A pre-authentication error produces no detailed wire diagnostic. Local typed exceptions retain safe diagnostic categories, excluding keys/plaintexts. No peer-controlled lengths or rate-table keys cause unbounded allocation. Application deadline overrides do not disable cryptographic checks.
## 15. Required vectors and implementation status
This file specifies candidate bytes and labels. Published Elligator vectors and [independent Python composition fixtures](CONCEALED-PROTOCOL-VECTORS.md) now cover exact F1–F4 bytes, signatures, role-assigned KX material, nonaligned continuous stream records and reordered UDP records. These passed on JVM, Kotlin/JS and Linux Native. Fixed F2–F4 fixtures compare production components; randomized integration tests exercise the complete state machines without a production entropy override. This is not a completed cryptographic review. Before freezing it:
1. Extend the pinned Elligator vector corpus and prove mixed-order agreement on every qualified platform; assess bignum timing and retained binary size.
2. Preserve and extend the independent F1–F4/KDF/signature/stream/UDP composition fixtures. A fully deterministic complete state-machine cross-implementation harness remains distinct from the completed component comparisons.
3. Verify malformed inputs, modified representative aliases, every authenticated length, incorrect roles/identities, low-order DH rejection, duplicate flights, replay eviction/restart, and key-usage boundaries.
4. Exercise initialization RPC readiness and authorization ordering against actual connection code before freezing integration behavior.
5. Independently review custom continuous encryption/MAC composition, KDF from concealed C, downgrade/identity binding, forward-secrecy scope, unauthenticated framing bounds, and rejection observability.
The first and second flights do not have forward secrecy against later compromise of server static encryption key e. Captured HELO then reveals C, which reveals the reply key and server hello. Fresh session DH still protects ACK/client identity and application traffic against that static-key compromise, assuming ephemeral secrets are erased and remain uncompromised. Server identity itself may already be publicly provisioned. No concealment guarantee covers traffic timing, endpoints, WebSocket setup, volume, or active probes possessing public server credentials.
## 16. Existing API evidence and primary references
Inspected crypto2 `SigningKey.sign`, `VerifyingKey.verify`, `SafeKeyExchange`, and `Hash`; common cached libsodium-binding 0.9.6 sources expose `GenericHash.genericHash(message, requestedHashLength, key)`, `SecretBox.easy/openEasy`, `ScalarMultiplication.scalarMultiplication`, and `Stream.chacha20XorIc(..., initialCounter: ULong, ...)`. High-level crypto2 default hash wrappers are not sufficient where this suite demands explicit output size/key. Exact per-platform behavior still needs runtime qualification.
- [Libsodium original ChaCha20 and counters](https://doc.libsodium.org/advanced/stream_ciphers/chacha20)
- [Libsodium generic hashing and explicit output size](https://doc.libsodium.org/hashing/generic_hashing)
- [Libsodium secretbox combined format](https://doc.libsodium.org/secret-key_cryptography/secretbox)
- [Libsodium crypto_kx roles and rejection](https://doc.libsodium.org/key_exchange)
- [Elligator key generation and subgroup randomization](https://elligator.org/key-exchange)
- [Monocypher reference Elligator API](https://monocypher.org/manual/elligator)
- [RFC 7748](https://www.rfc-editor.org/rfc/rfc7748.html)
### Primitive encoding check vectors
These small values were independently calculated using Python `hashlib.blake2b` (OpenSSL/Python implementation), not the Kotlin code. They validate selected byte encodings/KDF output parameters only; they are **not full handshake or Elligator vectors**.
```
secret/key = 000102030405060708090a0b0c0d0e0f
101112131415161718191a1b1c1d1e1f
context = a0a1a2a3
label = ASCII("vector")
Derive(32, secret, label, context) =
595859a9e444bb24d1df7c118eeef70fea86720249bd9292eba6e32744da6f19
Derive(24, secret, label, context) =
46d4b4de3827562b5654864913c2c46cb9802a6a71322599
record q = 7
body = 000000030161620000
L = 00000009
MAC using section12 exact domain and key above =
d0809fec300f48fb92d8eacd48284a8f
```
The record's objectBytes are `01 61 62`, with two zero padding bytes used solely for deterministic testing. Production padding is random. This vector treats object bytes opaquely and is not a valid RPC/control message example. A 24-byte BLAKE2b digest is not the first 24 bytes of its 32-byte digest; the first two vectors catch accidental truncation.