PALISADE v1.2 Wire Format Examples

This appendix provides complete, concrete, byte-level encodings of PALISADE v1.2 handshake messages. These examples serve four critical purposes:

  1. Interoperability — Independent implementations can verify they encode/decode correctly.
  2. Test Vectors — Developers can use these for unit tests or fuzzing harness seeds.
  3. Specification Clarity — CFRG/IETF reviewers expect at least one fully worked example.
  4. Security Validation — Transcript correctness depends on exact byte-for-byte encoding.

All values shown below are examples and not security-grade randomness. Real implementations MUST use cryptographically strong random values for all KEM keys and nonces.

Note: These examples use the PALISADE-PLUS profile (ML-KEM-768, ML-DSA-65) with actual algorithm sizes. The cryptographic outputs (hashes, signatures, ciphertexts) shown are placeholder patterns for serialization testing only.

1. ClientHelloPALISADE Example

1.1 Logical Inputs

version                 = 0x12         (PALISADE v1.2, single byte)
supported_kems_count    = 0x0001       (1 KEM)
supported_kems[0]       = 0x0011       (ML-KEM-768)
supported_sigs_count    = 0x0001       (1 signature algorithm)
supported_sigs[0]       = 0x0021       (ML-DSA-65)
supported_aeads_count   = 0x0001       (1 AEAD algorithm)
supported_aeads[0]       = 0x0001       (ChaCha20-Poly1305)
client_nonce            = 32 bytes (see test vector Section 2.1)
K_c                     = 1184 bytes (ML-KEM-768 public key, actual size)
client_certificate      = 1952 bytes (ML-DSA-65 public key, actual size)
client_signature        = (absent → length = 0x0000)
padding                 = (absent → length = 0x0000)
extensions              = (empty → count = 0x0000)

1.2 Serialized Form (Hex)

Below is a complete, deterministic encoding:

12                          // version = 0x12 (PALISADE v1.2)
00 01                       // supported_kems_count = 1
00 11                       // supported_kems[0] = ML-KEM-768 (0x0011)
00 01                       // supported_sigs_count = 1
00 21                       // supported_sigs[0] = ML-DSA-65 (0x0021)
00 01                       // supported_aeads_count = 1
00 01                       // supported_aeads[0] = ChaCha20-Poly1305 (0x0001)
01 02 03 04 05 06 07 08     // client_nonce[0:8]
09 0A 0B 0C 0D 0E 0F 10     // client_nonce[8:16]
11 12 13 14 15 16 17 18     // client_nonce[16:24]
19 1A 1B 1C 1D 1E 1F 20     // client_nonce[24:32] (32 bytes total)

04 A0                       // K_c length = 0x04A0 = 1184 bytes (ML-KEM-768)
AA AA AA AA ...             // [1184 bytes of 0xAA - placeholder pattern]

07 A0                       // client_certificate length = 0x07A0 = 1952 bytes (ML-DSA-65)
BB BB BB BB ...             // [1952 bytes of 0xBB - placeholder pattern]

00 00                       // client_signature length = 0 (absent)
00 00                       // padding length = 0 (absent)
00 00                       // extensions_count = 0 (no extensions)

Total Length: 3191 bytes

2. ServerHelloPALISADE Example

2.1 Logical Inputs

version             = 0x12         (negotiated v1.2, single byte)
kem_choice          = 0x0011       (ML-KEM-768)
sig_choice          = 0x0021       (ML-DSA-65)
aead_choice         = 0x0001       (ChaCha20-Poly1305)
server_nonce        = 32 bytes (see test vector Section 2.1)
K_s                 = 1184 bytes (ML-KEM-768 public key, actual size)
CT_c                = 1088 bytes (ML-KEM-768 ciphertext, actual size)
CT_s                = 1088 bytes (ML-KEM-768 ciphertext, actual size)
server_certificate  = 1952 bytes (ML-DSA-65 public key, actual size)
server_signature    = 3293 bytes (ML-DSA-65 signature, actual size)
dos_cookie          = (absent → length = 0x0000)
padding             = (absent → length = 0x0000)
extensions          = (empty → count = 0x0000)

2.2 Serialized Form (Hex)

12                          // version = 0x12 (negotiated v1.2)
00 11                       // kem_choice = ML-KEM-768 (0x0011)
00 21                       // sig_choice = ML-DSA-65 (0x0021)
00 01                       // aead_choice = ChaCha20-Poly1305 (0x0001)
A1 A2 A3 A4 A5 A6 A7 A8     // server_nonce[0:8]
A9 AA AB AC AD AE AF B0     // server_nonce[8:16]
B1 B2 B3 B4 B5 B6 B7 B8     // server_nonce[16:24]
B9 BA BB BC BD BE BF C0     // server_nonce[24:32] (32 bytes total)

04 A0                       // K_s length = 0x04A0 = 1184 bytes (ML-KEM-768)
BB BB BB BB ...             // [1184 bytes of 0xBB - placeholder pattern]

04 40                       // CT_c length = 0x0440 = 1088 bytes (ML-KEM-768 ciphertext)
CC CC CC CC ...             // [1088 bytes of 0xCC - placeholder pattern]

04 40                       // CT_s length = 0x0440 = 1088 bytes (ML-KEM-768 ciphertext)
DD DD DD DD ...             // [1088 bytes of 0xDD - placeholder pattern]

07 A0                       // server_certificate length = 0x07A0 = 1952 bytes (ML-DSA-65)
EE EE EE EE ...             // [1952 bytes of 0xEE - placeholder pattern]

0C E5                       // server_signature length = 0x0CE5 = 3293 bytes (ML-DSA-65)
FF FF FF FF ...             // [3293 bytes of 0xFF - placeholder pattern]

00 00                       // dos_cookie length = 0 (absent)
00 00                       // padding length = 0 (absent)
00 00                       // extensions_count = 0 (no extensions)

Total Length: 8661 bytes

3. Canonical Transcript Encoding

3.1 ClientHello Canonical Form

For transcript hashing, the canonical ClientHello excludes client_signature and padding fields entirely (PALISADE Spec Section 6.11, 7.5.1):

12                          // version
00 01                       // supported_kems_count
00 11                       // supported_kems[0]
00 01                       // supported_sigs_count
00 21                       // supported_sigs[0]
00 01                       // supported_aeads_count
00 01                       // supported_aeads[0]
01 02 03 04 05 06 07 08     // client_nonce[0:8]
09 0A 0B 0C 0D 0E 0F 10     // client_nonce[8:16]
11 12 13 14 15 16 17 18     // client_nonce[16:24]
19 1A 1B 1C 1D 1E 1F 20     // client_nonce[24:32]
04 A0                       // K_c length
[1184 bytes of 0xAA]        // K_c
07 A0                       // client_certificate length
[1952 bytes of 0xBB]        // client_certificate
00 00                       // extensions_count

Canonical ClientHello Length: 3191 bytes
SHA3-256 Hash: b49727dd99571698d31be3097908692eb5b412ffe832928e1da47c0f629b8267

3.2 ServerHello Canonical Form

For transcript hashing, the canonical ServerHello excludes server_signature, dos_cookie, and padding fields entirely (PALISADE Spec Section 6.11):

12                          // version
00 11                       // kem_choice
00 21                       // sig_choice
00 01                       // aead_choice
A1 A2 A3 A4 A5 A6 A7 A8     // server_nonce[0:8]
A9 AA AB AC AD AE AF B0     // server_nonce[8:16]
B1 B2 B3 B4 B5 B6 B7 B8     // server_nonce[16:24]
B9 BA BB BC BD BE BF C0     // server_nonce[24:32]
04 A0                       // K_s length
[1184 bytes of 0xBB]        // K_s
04 40                       // CT_c length
[1088 bytes of 0xCC]        // CT_c
04 40                       // CT_s length
[1088 bytes of 0xDD]        // CT_s
07 A0                       // server_certificate length
[1952 bytes of 0xEE]        // server_certificate
00 00                       // extensions_count

Canonical ServerHello Length: 5361 bytes
SHA3-256 Hash: 4ab280371aba93c1137061e799dc4f3b90c97f239b8d2bdfb560d19fc61629f5

3.3 Full Transcript Hash

When computing the main handshake transcript hash:

transcript = encode(ClientHello_canonical) || encode(ServerHello_canonical)
transcript_hash = SHA3-256(transcript)

The transcript hash is used for:

  • Key derivation: handshake_secret = HKDF-Expand(early_secret, label("handshake secret") || transcript_hash, 32)
  • Server signature: server_signature = SIG(server_privkey, transcript_hash)

4. Encrypted Packet Format Example

4.1 PublicHeader (30 bytes, cleartext)

51 50                    // magic = "QP" (0x5150)
12                       // version = 0x12 (v1.2)
00                       // flags = 0x00 (data frame, key_phase=0, reserved=0)
00 4E                    // length = 0x004E = 78 bytes (total packet including PublicHeader)
AA BB CC DD EE FF 00 11  // RID[0:8] (steering_prefix)
22 33 44 55 66 77 88 99  // RID[8:16]
AA BB CC DD EE FF 00 11  // RID[16:24] (privacy_suffix)

4.2 EncryptedHeader (16 bytes, before encryption)

00 00 00 00              // epoch_id = 0 (big-endian u32)
00 00 00 00 00 00 00 00  // seq = 0 (big-endian u64) - FIRST PACKET
00 00                    // padding_len = 0 (big-endian u16)
00                       // stream_id = 0
00                       // hdr_flags = 0x00 (reserved, must be 0)

4.3 Complete Packet Structure

[PublicHeader: 30 bytes]
[AEAD Ciphertext: EncryptedHeader (16) + payload + padding]
[AEAD Tag: 16 bytes]

Minimum Packet Sizes:
- Data packets: 30 + 16 + 16 = 62 bytes (empty payload, no padding)
- Control frame packets: 30 + 16 + 4 + 16 = 66 bytes (ControlFrame header: ctrl_type + length, even with empty ctrl_data)

5. Control Frame Example (CTRL_REKEY)

5.1 ControlFrame Structure (4 bytes, inside encrypted payload)

01                       // ctrl_type = CTRL_REKEY (1 byte)
00 00 00                 // length = 0x000000 (3 bytes, big-endian u24, length of ctrl_data only)
                         // ctrl_data = empty (0 bytes)

Note: The length field is a 24-bit unsigned integer in big-endian byte order. It specifies the length of ctrl_data only (not including ctrl_type or length fields). For an empty control frame, length = 0x000000, but the ControlFrame structure still occupies 4 bytes (ctrl_type + length = 1 + 3 bytes).

5.2 PublicHeader for Control Frame

51 50                    // magic = "QP"
12                       // version = 0x12
01                       // flags = 0x01 (control_frame=1, key_phase=0)
00 42                    // length = 0x0042 = 66 bytes total
[24 bytes RID]           // Routing Identifier

Total Control Frame Packet: 30 (PublicHeader) + 20 (ciphertext: EncryptedHeader 16 + ControlFrame 4) + 16 (tag) = 66 bytes

6. Key Differences from v1.1

The v1.2 wire format includes the following changes from v1.1:

  1. Removed msg_type field — Message type is implicit from context
  2. Version encoding — Changed from 2 bytes (0x01 0x01) to single byte (0x12)
  3. Algorithm negotiation — Changed from single kem_id/sig_id to lists: supported_kems[], supported_sigs[], supported_aeads[]
  4. Removed flags field — Capabilities now expressed through extensions
  5. Removed client_identity field — Identity now conveyed through client_certificate only
  6. Algorithm sizes — Updated to actual ML-KEM-768 (1184/1088) and ML-DSA-65 (1952/3293) sizes
  7. Canonical encodingclient_signature and padding are completely excluded from transcript (not just set to zero length)

7. Why These Examples Matter

Including wire-format examples in the spec:

  • Prevents divergent implementations (the #1 cause of handshake failures),
  • Allows developers to test serialization before implementing cryptography,
  • Satisfies CFRG expectations (they always ask for worked examples),
  • Provides automatic regression tests,
  • Demonstrates that the encoding rules are fully deterministic.

For a PQC-native protocol as new as PALISADE, these examples significantly strengthen credibility and auditability.

PALISADE Protocol Specification Draft 00

INFORMATIONAL