QUIB

Tags: wire, transport, session-flow

© R.A.Sol

HPPR QUIB transport uses a custom crypto layer instead of TLS 1.3.

Primitives:

No TLS, no X.509, no rust ring. Both peers must run this crypto.

Handshake

Two-message exchange inside QUIC Initial CRYPTO frames. Both messages travel in the Initial encryption space. After ECDH completes, each side writes a single 0x00 confirmation byte into Handshake CRYPTO to signal readiness, then upgrades to Data-space keys.

Message 1 (client to server, Initial CRYPTO)

client_ephemeral_pubkey (33 bytes, SEC1 compressed secp256k1)
client_transport_parameters (variable)

Message 2 (server to client, Initial CRYPTO)

server_ephemeral_pubkey (33 bytes, SEC1 compressed secp256k1)
encrypted_hello_length (2 bytes, big-endian u16)
encrypted_hello (variable, ChaCha20-Poly1305)
server_transport_parameters (variable)

The entire Message 2 is sent in a single CRYPTO frame. The client derives the shared secret from the server’s ephemeral pubkey, then decrypts the hello and parses transport parameters in one step.

Transport parameters use QUIC transport-parameter wire encoding.

Reference implementation note: the Rust implementation uses quinn-proto’s TransportParameters::write and TransportParameters::read helpers for this encoding.

Encrypted HELLO Payload

The server encrypts a HELLO payload into Message 2 using hello_key from the key derivation XOF stream (offset 256, see Key Derivation).

Encryption uses ChaCha20-Poly1305 with a zero nonce (unique key per connection).

Plaintext is the active service’s HELLO advertisement packet. At minimum it follows the generic HELLO metadata from 032: Command-Flow, the selected flow-specific command-list header, optional Transport headers, and any service-specific greeting headers.

Because the handshake is already connection-scoped, a service MAY omit a normally explicit session header here and derive equivalent binding from the transport instead.

The HPPR Repository Service sends its HELLO response payload here with Command-Flow: session, Repo-Name, Seal-By, optional PHC, repeated Transport, Session-Commands, and Allow-Null-Command: 0, and omits Session-ID because it derives the session id from QUIB keying material.

The encrypted hello includes a 16-byte Poly1305 tag appended to the ciphertext.

Post-handshake stream bootstrap

QUIB keeps standard QUIC endpoint roles:

All application streams are client-initiated. The HELLO payload is delivered during the handshake, so no server-initiated stream is needed.

  1. client reads the decrypted HELLO payload
  2. client derives any service-defined connection binding from keying material
  3. client opens a bidirectional stream and sends commands
  4. server accepts streams and dispatches commands

Reference implementation note: the Rust implementation exposes the HELLO payload through handshake_data(), uses accept_bi() for server streams, and uses open_bi() for client command and auxiliary streams.

Service Session Binding

service_session_binding is 32 bytes from the key derivation XOF stream offset 288 (see Key Derivation).

Higher-layer services MAY map this value into a connection-bound session token or other transport binding.

The HPPR Repository Service maps it to Session-ID: Q#<b64a> and uses it in place of the TAI-based session ids used on other transports.

The 🖧HELLO command remains available as an optional application-level request for capabilities refresh or service-specific status queries.

Key Agreement

shared_point = ECDH(client_ephemeral_secret, server_ephemeral_pubkey)
shared_bytes = shared_point.x (32 bytes, big-endian)

Both sides generate fresh ephemeral secp256k1 keypairs per connection.

Key Derivation

All keys for a connection are derived from the ECDH shared secret using a single BLAKE3 XOF stream:

stream = BLAKE3.derive_key_xof("hppr-🖧/quib/keys", shared_bytes)

Stream layout (368 bytes):

Offset Length Name
0 32 hs_c2s_packet
32 32 hs_s2c_packet
64 32 hs_c2s_header
96 32 hs_s2c_header
128 32 c2s_packet
160 32 s2c_packet
192 32 c2s_header
224 32 s2c_header
256 32 hello_key
288 32 service_session_binding
320 12 hs_c2s_iv
332 12 hs_s2c_iv
344 12 c2s_iv
356 12 s2c_iv

Handshake keys (hs_*) protect the Handshake packet space. Data keys protect the 1-RTT (Data) packet space.

Each packet key has a sibling IV derived from the same XOF stream. The IV is used in nonce construction (see Payload Encryption below).

The protocol performs two key upgrades during handshake: Initial to Handshake, then Handshake to Data. Each side writes a 0x00 confirmation byte into Handshake CRYPTO when returning 1-RTT keys, ensuring the peer receives a Handshake-space packet and completes the transition to Data.

Reference implementation note: quinn-proto drives these upgrades through the write_handshake callback.

QUIC Key Update

Each direction derives 44 bytes via BLAKE3 XOF from the current packet key:

stream = BLAKE3.derive_key_xof("hppr-🖧/quib/update", current_packet_key)
next_packet_key = stream[0..32]
next_iv          = stream[32..44]

Both key and IV are replaced. Header keys are not updated.

QUIC Payload Encryption

ChaCha20-Poly1305 (RFC 8439).

Nonce construction (12 bytes), following the TLS 1.3 pattern (RFC 9001 §5.3):

nonce = IV XOR (0x00000000 || BE64(packet_number))

The IV is the 12-byte sibling value derived alongside the packet key (from the XOF stream for initial/handshake/data keys, or from the key update XOF for rotated keys). The packet number is encoded as an 8-byte big-endian integer and XORed into the last 8 bytes of the IV.

Tag length: 16 bytes, appended to ciphertext.

Limits:

QUIC Header Protection

ChaCha20 mask generation per RFC 9001 §5.4.4.

Input: 16-byte sample from encrypted payload at offset pn_offset + 4.

counter = LE_u32(sample[0..4])
nonce   = sample[4..16]
mask    = ChaCha20(hp_key, counter, nonce, zeroes)[0..5]

Apply mask:

Sample size: 16 bytes.

Initial QUIC Keys

Before handshake completes, both peers derive identical keys from the destination connection ID using a single BLAKE3 XOF stream:

stream = BLAKE3.derive_key_xof("hppr-🖧/quib/initial", dst_cid)

Stream layout (152 bytes):

Offset Length Name
0 32 c2s_packet
32 32 s2c_packet
64 32 c2s_header
96 32 s2c_header
128 12 c2s_iv
140 12 s2c_iv

Initial keys use the same ChaCha20-Poly1305 and ChaCha20 algorithms as 1-RTT keys. Initial protection is not secret (the CID is on the wire).

Retry Integrity

Retry tags use BLAKE3 keyed MAC with a fixed public key, truncated to 16 bytes.

tag = BLAKE3.keyed_hash(RETRY_KEY, len(orig_dst_cid) || orig_dst_cid || retry_pseudo_packet)[..16]

RETRY_KEY is a fixed 32-byte constant compiled into both peers:

8a 3f c1 7b 52 e6 d9 04  ab 1e 73 f0 28 95 dc 46
b3 67 0a 5d e4 89 f1 3c  7e b2 05 6f d8 a1 43 97

This mirrors RFC 9001’s public retry integrity key. The tag is tamper-detection, not a secret.

HMAC

BLAKE3 keyed hash with 32-byte output.

mac = BLAKE3.keyed_hash(key, data)

Verification compares all 32 bytes.

Token Encryption

Address validation tokens use per-token derived ChaCha20-Poly1305.

Key derivation:

aead_key = BLAKE3.derive_key("hppr-🖧/quib/token-aead", master_key || random_bytes)

Seal and open use a zero nonce. Each token has a unique derived key, making nonce reuse impossible.

Export Keying Material

output = BLAKE3.derive_key(label, shared_bytes || context)

label is interpreted as a UTF-8 string for the BLAKE3 context parameter. Output length is variable via BLAKE3 XOF.

Peer Identity

The peer identity is the peer’s ephemeral SEC1 compressed secp256k1 public key (33 bytes).

For clients, the HELLO payload is the active service’s greeting packet. For the HPPR Repository Service this includes the repo verifier and advertised capabilities.

Reference implementation note: the Rust implementation exposes the peer public key through peer_identity() and the transport greeting through handshake_data().

No 0-RTT

Early data is not supported.

Reference implementation note: the Rust implementation returns None from early_crypto.

Context Strings

All BLAKE3 derive_key / XOF context strings used by this spec:

Context Use
hppr-🖧/quib/keys XOF for all handshake/data keys, IVs, hello key, service session binding
hppr-🖧/quib/initial XOF for initial keys and IVs
hppr-🖧/quib/update XOF for key update (packet key + IV per direction)
hppr-🖧/quib/token-aead Token AEAD key derivation