QUIB
Tags: wire, transport, session-flow
© R.A.Sol
HPPR QUIB transport uses a custom crypto layer instead of TLS 1.3.
Primitives:
- Key agreement: secp256k1 ECDH
- Key derivation: BLAKE3
derive_key - QUIC payload encryption: ChaCha20-Poly1305
- QUIC header protection: ChaCha20 (RFC 9001 §5.4.4 pattern)
- MAC: BLAKE3 keyed hash
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:
- the client initiates the connection (
connect) - the server accepts the connection (
accept)
All application streams are client-initiated. The HELLO payload is delivered during the handshake, so no server-initiated stream is needed.
- client reads the decrypted HELLO payload
- client derives any service-defined connection binding from keying material
- client opens a bidirectional stream and sends commands
- 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:
- Confidentiality: 2^62 QUIC packets
- Integrity: 2^36 decryption failures
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:
- Long headers:
header[0] ^= mask[0] & 0x0f - Short headers:
header[0] ^= mask[0] & 0x1f header[pn_offset .. pn_offset+pn_len] ^= mask[1..1+pn_len]
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 |