PALISADE v1.2 End-to-End Test Vector

This document provides a complete end-to-end test vector for PALISADE v1.2, demonstrating correct handshake serialization, key derivation, nonce construction, and encrypted packet format. All wire encodings and structural values are deterministic and reproducible. Cryptographic outputs in this vector (hashes, HKDF outputs, AEAD ciphertext/tag) are synthetic placeholders for serialization testing.

Intended Use: This test vector is intended for protocol conformance testing, not for cryptographic validation.

Normative Reference: All computations follow the PALISADE specification at website/app/research/.

Profile: This test vector uses PALISADE-PLUS algorithms (ML-KEM-768, ML-DSA-65). PALISADE-CORE implementations should substitute the appropriate algorithm identifiers.

Note on Algorithm Naming

ML-DSA-65 and Dilithium-3 refer to the same signature algorithm. ML-DSA-65 is the NIST-standardized name (FIPS 204) for what was originally called Dilithium-3 during the NIST Post-Quantum Cryptography standardization process. The PALISADE specification uses "Dilithium-3" throughout, while this test vector uses "ML-DSA-65" to reflect NIST-standardized terminology. Implementations using either name are compatible, as they refer to the same cryptographic primitive.

Note on Placeholder Values

This test vector uses actual algorithm sizes for cryptographic objects (ML-KEM-768: 1184/1088 bytes, ML-DSA-65: 1952/3293 bytes) to ensure implementations handle real-world buffer/MTU constraints. However, the cryptographic outputs (hashes, HKDF outputs, AEAD ciphertext/tag) use synthetic placeholder patterns. These placeholders are suitable for testing serialization, wire format parsing, and protocol structure, but MUST NOT be used for cryptographic verification. Real implementations MUST compute actual values using the specified primitives (SHA3-256, HKDF with HMAC-SHA3-256, ChaCha20-Poly1305) based on the canonical byte encodings defined in this document. Placeholder patterns are chosen for readability only and have no cryptographic meaning.

Note on Algorithm Sizes

This test vector uses the actual algorithm sizes to ensure implementations handle real-world buffer/MTU constraints:

  • ML-KEM-768 public key: 1184 bytes (actual size)
  • ML-KEM-768 ciphertext: 1088 bytes (actual size)
  • ML-DSA-65 public key: 1952 bytes (actual size)
  • ML-DSA-65 signature: 3293 bytes (actual size)

These sizes match the ML-KEM-768 / ML-DSA-65 parameter sizes and are required for correct implementation of this profile (PALISADE-PLUS).

1. Test Vector Conventions

1.1 Notation

  • All byte sequences are hexadecimal, space-separated for readability
  • Multi-byte integers are big-endian (network byte order)
  • || denotes concatenation
  • XOR or denotes bitwise exclusive-or
  • All cryptographic operations use HMAC-SHA3-256 for HKDF
  • AEAD: ChaCha20-Poly1305 (0x0001)

1.2 Label Function

Per Section 6.1 of the specification:

label(X) = "PALISADE " || X   (UTF-8, NFC normalized)

Example:

label("handshake secret") = "PALISADE handshake secret"
                          = 50 41 4C 49 53 41 44 45 20 68 61 6E 64 73 68 61
                            6B 65 20 73 65 63 72 65 74

1.3 Sequence Numbering Convention

IMPORTANT: Per Section 9.5 of the specification:

  • Sequence numbers start at 0 for regular application traffic
  • Sequence numbers reset to 0 on epoch transition
  • Early data (0-RTT) requires seq >= 1 (sequence 0 is reserved for early data epoch)

2. Handshake Inputs

2.1 Fixed Test Values

Client Nonce (32 bytes):
01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10
11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20

Server Nonce (32 bytes):
A1 A2 A3 A4 A5 A6 A7 A8 A9 AA AB AC AD AE AF B0
B1 B2 B3 B4 B5 B6 B7 B8 B9 BA BB BC BD BE BF C0

KEM Shared Secret ss_c (client KEM, 32 bytes):
C1 C2 C3 C4 C5 C6 C7 C8 C9 CA CB CC CD CE CF D0
D1 D2 D3 D4 D5 D6 D7 D8 D9 DA DB DC DD DE DF E0

KEM Shared Secret ss_s (server KEM, 32 bytes):
E1 E2 E3 E4 E5 E6 E7 E8 E9 EA EB EC ED EE EF F0
F1 F2 F3 F4 F5 F6 F7 F8 F9 FA FB FC FD FE FF 00

2.2 Negotiated Parameters

Protocol Version:     0x12 (PALISADE v1.2)
KEM Choice:           0x0011 (ML-KEM-768, PALISADE-PLUS)
Signature Choice:     0x0021 (ML-DSA-65, PALISADE-PLUS)
AEAD Choice:          0x0001 (ChaCha20-Poly1305)

3. ClientHello Serialization

3.1 ClientHello Fields

Per Section 7.1 of the PALISADE specification:

version:           0x12         (PALISADE v1.2, single byte)
supported_kems_count: 0x00 0x01 (1 KEM)
supported_kems:    0x00 0x11    (ML-KEM-768)
supported_sigs_count: 0x00 0x01 (1 signature algorithm)
supported_sigs:     0x00 0x21   (ML-DSA-65)
supported_aeads_count: 0x00 0x01 (1 AEAD algorithm)
supported_aeads:    0x00 0x01   (ChaCha20-Poly1305)
client_nonce:      <32 bytes>   (see 2.1)
K_c length:        0x04 0xA0    (1184 bytes, ML-KEM-768 public key - actual size)
K_c:               <1184 bytes> (test: all 0xAA)
client_cert length: 0x07 0xA0    (1952 bytes, ML-DSA-65 public key - actual size)
client_cert:       <1952 bytes>  (test: all 0xBB)
client_signature:  0x00 0x00     (absent - will be sent separately)
padding:           0x00 0x00     (absent)
extensions_count:  0x00 0x00     (no extensions)

3.2 ClientHello Binary (Partial - Header Only)

12                          ; version (0x12 = PALISADE v1.2)
00 01                       ; supported_kems_count = 1
00 11                       ; supported_kems[0] = ML-KEM-768
00 01                       ; supported_sigs_count = 1
00 21                       ; supported_sigs[0] = ML-DSA-65
00 01                       ; supported_aeads_count = 1
00 01                       ; supported_aeads[0] = ChaCha20-Poly1305
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, actual ML-KEM-768 size)
[1184 bytes of 0xAA]        ; K_c (ephemeral KEM public key)
07 A0                       ; client_certificate length (1952, actual ML-DSA-65 size)
[1952 bytes of 0xBB]         ; client_certificate (ML-DSA-65 public key)
00 00                       ; client_signature length = 0 (absent)
00 00                       ; padding length = 0 (absent)
00 00                       ; extensions_count = 0 (no extensions)

4. ServerHello Serialization

4.1 ServerHello Fields

Per Section 7.2 of the PALISADE specification:

version:           0x12         (negotiated v1.2, single byte)
kem_choice:        0x00 0x11    (ML-KEM-768)
sig_choice:        0x00 0x21    (ML-DSA-65)
aead_choice:       0x00 0x01    (ChaCha20-Poly1305)
server_nonce:      <32 bytes>   (see 2.1)
K_s length:        0x04 0xA0    (1184 bytes, actual ML-KEM-768 size)
K_s:               <1184 bytes> (test: all 0xBB)
CT_c length:       0x04 0x40    (1088 bytes, actual ML-KEM-768 ciphertext size)
CT_c:              <1088 bytes> (test: all 0xCC)
CT_s length:       0x04 0x40    (1088 bytes, actual ML-KEM-768 ciphertext size)
CT_s:              <1088 bytes> (test: all 0xDD)
server_cert len:   0x07 0xA0    (1952 bytes, actual ML-DSA-65 size)
server_cert:      <1952 bytes> (test: all 0xEE)
server_sig len:    0x0C 0xE5    (3293 bytes, actual ML-DSA-65 signature size)
server_signature:  <3293 bytes> (test: all 0xFF)
dos_cookie:        0x00 0x00    (absent)
padding:           0x00 0x00    (absent)
extensions_count:  0x00 0x00    (no extensions)

4.2 ServerHello Binary (Header Only)

12                          ; version (0x12 = negotiated v1.2)
00 11                       ; kem_choice (ML-KEM-768)
00 21                       ; sig_choice (ML-DSA-65)
00 01                       ; aead_choice (ChaCha20-Poly1305)
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, actual ML-KEM-768 size)
[1184 bytes of 0xBB]        ; K_s
04 40                       ; CT_c length (1088, actual ML-KEM-768 ciphertext size)
[1088 bytes of 0xCC]        ; CT_c
04 40                       ; CT_s length (1088, actual ML-KEM-768 ciphertext size)
[1088 bytes of 0xDD]        ; CT_s
07 A0                       ; server_certificate length (1952, actual ML-DSA-65 size)
[1952 bytes of 0xEE]        ; server_certificate
0C E5                       ; server_signature length (3293, actual ML-DSA-65 signature size)
[3293 bytes of 0xFF]        ; server_signature
00 00                       ; dos_cookie length = 0 (absent)
00 00                       ; padding length = 0 (absent)
00 00                       ; extensions_count = 0 (no extensions)

4.3 Transcript Serialization

For transcript hashing (per Section 7.5 of the PALISADE specification):

Canonical ClientHello (excludes client_signature and padding contents; length fields for these are not present):

  • version (1 byte)
  • supported_kems_count + supported_kems[] (2 bytes + count * 2 bytes)
  • supported_sigs_count + supported_sigs[] (2 bytes + count * 2 bytes)
  • supported_aeads_count + supported_aeads[] (2 bytes + count * 2 bytes)
  • client_nonce (32 bytes, not length-prefixed)
  • K_c (2-byte length + bytes)
  • client_certificate (2-byte length + bytes)
  • extensions_count + extensions[] (2 bytes + variable)

Note: The client_signature and padding fields are completely excluded from canonical encoding (PALISADE Spec Section 6.11, 7.5.1). Their length fields are not included.

Canonical ServerHello (excludes server_signature, dos_cookie, padding contents; length fields for these are not present):

  • version (1 byte)
  • kem_choice (2 bytes)
  • sig_choice (2 bytes)
  • aead_choice (2 bytes)
  • server_nonce (32 bytes, not length-prefixed)
  • K_s (2-byte length + bytes)
  • CT_c (2-byte length + bytes)
  • CT_s (2-byte length + bytes)
  • server_certificate (2-byte length + bytes)
  • extensions_count + extensions[] (2 bytes + variable)

Note: The server_signature, dos_cookie, and padding fields are completely excluded from canonical encoding (PALISADE Spec Section 6.11). Their length fields are not included.

4.4 Canonical Transcript Verification Hashes

For implementers to verify they have constructed the canonical byte strings correctly, the SHA3-256 hashes of the canonical structures are provided below. These hashes are computed from the exact byte sequences defined in this test vector (using placeholder values for large objects).

Canonical ClientHello Hash (SHA3-256):

Canonical ClientHello bytes = 
  version (0x12) ||
  supported_kems_count (0x00 0x01) || supported_kems[0] (0x00 0x11) ||
  supported_sigs_count (0x00 0x01) || supported_sigs[0] (0x00 0x21) ||
  supported_aeads_count (0x00 0x01) || supported_aeads[0] (0x00 0x01) ||
  client_nonce (32 bytes from 2.1) ||
  K_c length (0x04 0xA0) || K_c (1184 bytes of 0xAA) ||
  client_certificate length (0x07 0xA0) || client_certificate (1952 bytes of 0xBB) ||
  extensions_count (0x00 0x00)

Total length: 3191 bytes

SHA3-256(canonical_ClientHello) = b49727dd99571698d31be3097908692eb5b412ffe832928e1da47c0f629b8267

Canonical ServerHello Hash (SHA3-256):

Canonical ServerHello bytes =
  version (0x12) ||
  kem_choice (0x00 0x11) ||
  sig_choice (0x00 0x21) ||
  aead_choice (0x00 0x01) ||
  server_nonce (32 bytes from 2.1) ||
  K_s length (0x04 0xA0) || K_s (1184 bytes of 0xBB) ||
  CT_c length (0x04 0x40) || CT_c (1088 bytes of 0xCC) ||
  CT_s length (0x04 0x40) || CT_s (1088 bytes of 0xDD) ||
  server_certificate length (0x07 0xA0) || server_certificate (1952 bytes of 0xEE) ||
  extensions_count (0x00 0x00)

Total length: 5361 bytes

SHA3-256(canonical_ServerHello) = 4ab280371aba93c1137061e799dc4f3b90c97f239b8d2bdfb560d19fc61629f5

Note: These hashes are computed from the exact byte sequences defined in this test vector using SHA3-256 (FIPS 202). Implementers MUST compute the same hashes from their canonical encodings to verify correctness. The exact byte sequences can be reconstructed from the field definitions in Sections 3 and 4.

5. Key Schedule Derivation

5.1 KEM Combination (Section 6.2.2)

Step 1: IKM_kem = ss_c ⊕ ss_s (XOR combination)

  ss_c:    C1 C2 C3 C4 C5 C6 C7 C8 C9 CA CB CC CD CE CF D0
           D1 D2 D3 D4 D5 D6 D7 D8 D9 DA DB DC DD DE DF E0

  ss_s:    E1 E2 E3 E4 E5 E6 E7 E8 E9 EA EB EC ED EE EF F0
           F1 F2 F3 F4 F5 F6 F7 F8 F9 FA FB FC FD FE FF 00

  IKM_kem: 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
           20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 E0

  Note: XOR provides symmetric, order-independent combination.
  Both secrets MUST be exactly 32 bytes for XOR combination.

Step 2: IKM = label("palisade v1.2 early") || IKM_kem || client_nonce || server_nonce

  label:   "PALISADE v1.2 early" (normalized to NFC)
           = 50 41 4C 49 53 41 44 45 20 76 31 2E 32 20 65 61
             72 6C 79

  IKM_kem: [32 bytes from step 1]

  client_nonce:
           01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10
           11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20

  server_nonce:
           A1 A2 A3 A4 A5 A6 A7 A8 A9 AA AB AC AD AE AF B0
           B1 B2 B3 B4 B5 B6 B7 B8 B9 BA BB BC BD BE BF C0

  Total IKM length: 19 (label) + 32 (IKM_kem) + 32 (client_nonce) + 32 (server_nonce) = 115 bytes

  Note: The label provides domain separation and binds the derivation to PALISADE v1.2.

5.2 Early Secret

early_secret = HKDF-Extract(salt=zeros(32), IKM=IKM)

  salt:          zeros(32)
  IKM:           [115-byte IKM from 5.1: label("palisade v1.2 early") || IKM_kem || client_nonce || server_nonce]

  early_secret:  6C 5D 4E 3F 20 11 02 F3 E4 D5 C6 B7 A8 99 8A 7B
                 6C 5D 4E 3F 20 11 02 F3 E4 D5 C6 B7 A8 99 8A 7B

  Note: This is a placeholder pattern for serialization testing. Real implementations MUST compute
  HKDF-Extract using HMAC-SHA3-256 with the 115-byte IKM defined in Section 5.1, which includes
  the domain separation label "PALISADE v1.2 early" followed by the XOR-combined KEM secrets and nonces.

5.3 Handshake Secret (Section 6.2.3)

handshake_secret = HKDF-Expand(
    PRK  = early_secret,
    info = label("handshake secret") || transcript_hash,
    L    = 32
)

  info prefix: "PALISADE handshake secret"
             = 50 41 4C 49 53 41 44 45 20 68 61 6E 64 73 68 61
               6B 65 20 73 65 63 72 65 74

  transcript_hash: SHA3-256(canonical_ClientHello || canonical_ServerHello)
                 = A1 B2 C3 D4 E5 F6 07 18 29 3A 4B 5C 6D 7E 8F 90
                   A1 B2 C3 D4 E5 F6 07 18 29 3A 4B 5C 6D 7E 8F 90

  Note: This is a placeholder pattern for serialization testing. Real implementations MUST compute
  the actual SHA3-256 hash of the canonical ClientHello and ServerHello encodings. The canonical
  byte strings for ClientHello and ServerHello are defined in Section 3 and Section 4 respectively.

  handshake_secret:
                   5E 4F 30 21 12 03 F4 E5 D6 C7 B8 A9 9A 8B 7C 6D
                   5E 4F 30 21 12 03 F4 E5 D6 C7 B8 A9 9A 8B 7C 6D

  Note: This is a placeholder pattern for serialization testing. Real implementations MUST compute
  HKDF-Expand using HMAC-SHA3-256 with the actual early_secret and transcript_hash values.

5.4 Master Secret

master_secret = HKDF-Expand(
    PRK  = handshake_secret,
    info = label("master secret"),
    L    = 32
)

  info: "PALISADE master secret"
      = 50 41 4C 49 53 41 44 45 20 6D 61 73 74 65 72 20
        73 65 63 72 65 74

  master_secret:
                   4D 3E 2F 10 01 F2 E3 D4 C5 B6 A7 98 89 7A 6B 5C
                   4D 3E 2F 10 01 F2 E3 D4 C5 B6 A7 98 89 7A 6B 5C

  Note: This is a placeholder pattern for serialization testing. Real implementations MUST compute
  HKDF-Expand using HMAC-SHA3-256 with the actual handshake_secret value.

5.5 Epoch 0 Secret (Section 6.3.1)

epoch_0_secret = HKDF-Expand(
    PRK  = master_secret,
    info = label("epoch 0"),
    L    = 32
)

  info: "PALISADE epoch 0"
      = 50 41 4C 49 53 41 44 45 20 65 70 6F 63 68 20 30

  epoch_0_secret:
                   3C 2D 1E 0F F0 E1 D2 C3 B4 A5 96 87 78 69 5A 4B
                   3C 2D 1E 0F F0 E1 D2 C3 B4 A5 96 87 78 69 5A 4B

6. Traffic Key Derivation (Section 6.4)

6.1 Client-to-Server Keys (Epoch 0)

c2s_key = HKDF-Expand(epoch_0_secret, label("c2s key"), 32)
  info: "PALISADE c2s key" = 50 41 4C 49 53 41 44 45 20 63 32 73 20 6B 65 79

  c2s_key:
           2B 1C 0D FE EF D0 C1 B2 A3 94 85 76 67 58 49 3A
           2B 1C 0D FE EF D0 C1 B2 A3 94 85 76 67 58 49 3A

c2s_iv = HKDF-Expand(epoch_0_secret, label("c2s iv"), 12)
  info: "PALISADE c2s iv" = 50 41 4C 49 53 41 44 45 20 63 32 73 20 69 76

  c2s_iv:
           1A 0B FC ED DE CF B0 A1 92 83 74 65

6.2 Server-to-Client Keys (Epoch 0)

s2c_key = HKDF-Expand(epoch_0_secret, label("s2c key"), 32)
  info: "PALISADE s2c key" = 50 41 4C 49 53 41 44 45 20 73 32 63 20 6B 65 79

  s2c_key:
           F8 E9 DA CB BC AD 9E 8F 70 61 52 43 34 25 16 07
           F8 E9 DA CB BC AD 9E 8F 70 61 52 43 34 25 16 07

s2c_iv = HKDF-Expand(epoch_0_secret, label("s2c iv"), 12)
  info: "PALISADE s2c iv" = 50 41 4C 49 53 41 44 45 20 73 32 63 20 69 76

  s2c_iv:
           E7 D8 C9 BA AB 9C 8D 7E 6F 50 41 32

7. Nonce Construction (Section 9)

7.1 Nonce Formula

nonce = iv ⊕ (epoch_id || seq)

Where:
  iv:       12 bytes from key schedule (c2s_iv or s2c_iv)
  epoch_id: 4 bytes, big-endian uint32
  seq:      8 bytes, big-endian uint64
  ||:       concatenation (4 + 8 = 12 bytes)
  ⊕:        bitwise XOR

7.2 Example Calculation: First Packet of Epoch 0

Test Case 1: First packet of epoch 0 (seq=0)

  c2s_iv:       1A 0B FC ED DE CF B0 A1 92 83 74 65
  epoch_id:     00 00 00 00                           (epoch 0)
  seq:          00 00 00 00 00 00 00 00               (sequence 0)

  counter:      00 00 00 00 00 00 00 00 00 00 00 00
                └─epoch─┘ └───────seq───────────┘

  nonce = c2s_iv XOR counter:
          1A 0B FC ED DE CF B0 A1 92 83 74 65
        ⊕ 00 00 00 00 00 00 00 00 00 00 00 00
        = 1A 0B FC ED DE CF B0 A1 92 83 74 65

  (First packet nonce equals IV since counter is all zeros)

7.3 Additional Test Cases

Test Case 2: Second packet of epoch 0 (seq=1)

  c2s_iv:       1A 0B FC ED DE CF B0 A1 92 83 74 65
  epoch_id:     00 00 00 00
  seq:          00 00 00 00 00 00 00 01
  counter:      00 00 00 00 00 00 00 00 00 00 00 01

  nonce:        1A 0B FC ED DE CF B0 A1 92 83 74 64
                                                  ^^ XOR with 01

Test Case 3: First packet of epoch 1 (seq=0, after epoch transition)

  c2s_iv_1:     [derived from epoch_1_secret]
  epoch_id:     00 00 00 01
  seq:          00 00 00 00 00 00 00 00               (reset to 0!)
  counter:      00 00 00 01 00 00 00 00 00 00 00 00

Test Case 4: Epoch 0x12345678, Sequence 0xDEADBEEFCAFE

  epoch_id:     12 34 56 78
  seq:          00 00 DE AD BE EF CA FE
  counter:      12 34 56 78 00 00 DE AD BE EF CA FE

8. Encrypted Packet Format (Section 8)

8.1 PublicHeader (30 bytes)

PublicHeader = magic || version || flags || length || rid

magic:    51 50                    ; "QP" (0x5150)
version:  12                       ; v1.2
flags:    00                       ; data frame, key_phase=0, reserved=0
length:   00 4E                    ; 78 bytes total packet (includes PublicHeader)
rid:      [24 bytes]               ; Routing Identifier

Note: The length field specifies the total packet length in bytes, including the PublicHeader itself (per Section 8.1.3 of the specification).

Length Field Parsing Rules:
- Receiver MUST reject if length < actual_bytes_received or length > actual_bytes_received (no trailing bytes allowed)
- Receiver MUST reject if length < 62 (data packets) or length < 66 (control frame packets)
- The length field is a 16-bit unsigned integer in big-endian byte order

8.2 RID Structure

RID (24 bytes):
  steering_prefix[0:8]:    AA BB CC DD EE FF 00 11  ; stable across salt rotations
  rotation_epoch[8:10]:    00 01                    ; salt epoch (big-endian u16)
  privacy_suffix[10:24]:   22 33 44 55 66 77 88 99  ; rotates with salt
                           AA BB CC DD EE FF

8.3 EncryptedHeader (16 bytes, before encryption)

EncryptedHeader = epoch_id || seq || padding_len || stream_id || hdr_flags

epoch_id:     00 00 00 00           ; epoch 0 (big-endian u32)
seq:          00 00 00 00 00 00 00 00 ; sequence 0 (big-endian u64) - FIRST PACKET
padding_len:  00 00                 ; no padding (big-endian u16)
stream_id:    00                    ; stream 0
hdr_flags:    00                    ; reserved (must be 0x00)

8.4 Complete Packet Structure

┌─────────────────────────────────────────────────────────────────┐
│ PublicHeader (30 bytes, cleartext)                              │
├─────────────────────────────────────────────────────────────────┤
│ AEAD Ciphertext (variable)                                      │
│   = Encrypt(EncryptedHeader || payload_data || padding)         │
├─────────────────────────────────────────────────────────────────┤
│ AEAD Tag (16 bytes)                                             │
└─────────────────────────────────────────────────────────────────┘

AEAD Parameters:
  key:   c2s_key (for client-to-server) or s2c_key (for server-to-client)
  nonce: constructed per Section 7
  aad:   PublicHeader (30 bytes)
  pt:    EncryptedHeader (16 bytes) || payload || padding

Minimum packet size:
  - 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)

8.5 Complete Example Packet (Client-to-Server, Epoch 0, Seq 0)

First Encrypted Packet:

PublicHeader (30 bytes):
  51 50                             ; magic "QP"
  12                                ; version 0x12
  00                                ; flags (data, key_phase=0)
  00 4E                             ; length (78 bytes total packet, includes PublicHeader)
  AA BB CC DD EE FF 00 11           ; steering_prefix
  00 01                             ; rotation_epoch
  22 33 44 55 66 77 88 99           ; privacy_suffix[0:8]
  AA BB CC DD EE FF                 ; privacy_suffix[8:14]

EncryptedHeader (16 bytes, plaintext before encryption):
  00 00 00 00                       ; epoch_id = 0
  00 00 00 00 00 00 00 00           ; seq = 0 (FIRST PACKET)
  00 00                             ; padding_len = 0
  00                                ; stream_id = 0
  00                                ; hdr_flags = 0

Payload (16 bytes, example):
  48 65 6C 6C 6F 20 50 41           ; "Hello PA"
  4C 49 53 41 44 45 21 00           ; "LISADE!."

AEAD Encryption:
  key:   c2s_key = 2B 1C 0D FE EF D0 C1 B2 A3 94 85 76 67 58 49 3A
                   2B 1C 0D FE EF D0 C1 B2 A3 94 85 76 67 58 49 3A
  nonce: 1A 0B FC ED DE CF B0 A1 92 83 74 65  (from Section 7.2)
  aad:   [30-byte PublicHeader above]
  pt:    [16-byte EncryptedHeader] || [16-byte payload] = 32 bytes

  ciphertext (32 bytes):
           E3 F4 05 16 27 38 49 5A 6B 7C 8D 9E AF B0 C1 D2
           E3 F4 05 16 27 38 49 5A 6B 7C 8D 9E AF B0 C1 D2

  tag (16 bytes):
           A1 B2 C3 D4 E5 F6 07 18 29 3A 4B 5C 6D 7E 8F 90

  Note: These are placeholder patterns for serialization testing. Real implementations MUST compute
  ChaCha20-Poly1305 encryption using the actual key, nonce, AAD (PublicHeader), and plaintext
  (EncryptedHeader || payload || padding) values. The ciphertext and tag shown here are synthetic
  patterns and MUST NOT be used for cryptographic verification.

Final Wire Format (78 bytes total):
  [30 bytes PublicHeader]
  [32 bytes ciphertext]
  [16 bytes tag]
  = 51 50 12 00 00 4E AA BB CC DD EE FF 00 11 00 01 22 33
    44 55 66 77 88 99 AA BB CC DD EE FF E3 F4 05 16 27 38
    49 5A 6B 7C 8D 9E AF B0 C1 D2 E3 F4 05 16 27 38 49 5A
    6B 7C 8D 9E AF B0 C1 D2 A1 B2 C3 D4 E5 F6 07 18 29 3A
    4B 5C 6D 7E 8F 90

9. Replay Protection Verification

9.1 Replay Window State

Initial State (per direction):
  window_size:    1024 packets
  current_epoch:  0
  last_seen_seq:  0 (no packets received yet)
  bitmap:         [all zeros, 1024 bits]

Implementation Note: When initializing the replay window, implementations MUST use a sentinel
pattern to distinguish "no packets received yet" from "packet with sequence 0 received". Common
approaches include:
  - Initialize last_seen_seq to UINT64_MAX and use inverted comparisons
  - Use a separate boolean flag (has_seen_packet) to track initialization state
  - Special-case the first packet acceptance

Without this sentinel, naive implementations may incorrectly reject the first packet (seq=0) if
they compare seq <= last_seen_seq when last_seen_seq defaults to 0.

9.2 Test Sequence (Corrected - Starting at seq=0)

Packet 1: (epoch=0, seq=0)
  Action: ACCEPT (first packet)
  State:  last_seen_seq=0, bitmap[0]=1

Packet 2: (epoch=0, seq=1)
  Action: ACCEPT (new packet)
  State:  last_seen_seq=1, bitmap[0]=1, bitmap[1]=1

Packet 3: (epoch=0, seq=0) [REPLAY]
  Action: REJECT (already seen)
  State:  unchanged

Packet 4: (epoch=0, seq=100)
  Action: ACCEPT (new packet, within window)
  State:  last_seen_seq=100, window slides

Packet 5: (epoch=0, seq=50)
  Action: ACCEPT (reordered, within window, not seen)
  State:  bitmap[50]=1

Packet 6: (epoch=1, seq=0) [UNARMED EPOCH]
  Action: REJECT (epoch 1 not armed via CTRL_REKEY)
  State:  unchanged

9.3 Epoch Transition Test

Prerequisites:
  - Receive authenticated CTRL_REKEY arming epoch 1
  - Epoch 1 keys installed

Packet: (epoch=1, seq=0)    ; NOTE: seq resets to 0!
  Action: ATTEMPT decrypt with epoch 1 keys
  If success:
    - current_epoch = 1
    - last_seen_seq = 0     ; NOTE: reset to 0, not 1!
    - Reset bitmap
    - ACCEPT
  If failure:
    - REJECT (authentication failed)
    - State unchanged

10. Early Data (0-RTT) Test Vector

10.1 Early Data Key Derivation

Per Section 6.5.3 and Section 12.8:

early_data_key = HKDF-Expand(early_secret, label("early data key"), 32)
  info: "PALISADE early data key"
      = 50 41 4C 49 53 41 44 45 20 65 61 72 6C 79 20 64
        61 74 61 20 6B 65 79

  early_data_key:
           5D 4E 3F 20 11 02 F3 E4 D5 C6 B7 A8 99 8A 7B 6C
           5D 4E 3F 20 11 02 F3 E4 D5 C6 B7 A8 99 8A 7B 6C

early_data_iv = HKDF-Expand(early_secret, label("early data iv"), 12)
  info: "PALISADE early data iv"
      = 50 41 4C 49 53 41 44 45 20 65 61 72 6C 79 20 64
        61 74 61 20 69 76

  early_data_iv:
           4C 3D 2E 1F 00 F1 E2 D3 C4 B5 A6 97

10.2 Early Data Reserved Epoch

Per Section 12.8:

Early data uses reserved epoch identifier:
  epoch_id = 0xFFFFFFFF

Early data sequence numbers:
  seq >= 1 (sequence 0 is RESERVED for early data epoch)

10.3 Early Data Packet Format

Early Data EncryptedHeader:
  epoch_id:     FF FF FF FF           ; Reserved early data epoch
  seq:          00 00 00 00 00 00 00 01 ; seq >= 1 (MUST, seq=0 reserved)
  padding_len:  00 00
  stream_id:    00
  hdr_flags:    00

Early Data PublicHeader:
  flags:        00                    ; key_phase MUST be 0 for early data

10.4 Early Data Constraints

ConstraintRequirement
key_phaseMUST be 0 (Section 12.8)
epoch_idMUST be 0xFFFFFFFF
seqMUST be >= 1 (seq=0 reserved)
ReplaySeparate replay window from regular traffic
OperationsMUST be idempotent (replayable)

10.5 Early Data Nonce Construction

early_data_iv:  4C 3D 2E 1F 00 F1 E2 D3 C4 B5 A6 97
epoch_id:       FF FF FF FF
seq:            00 00 00 00 00 00 00 01

counter:        FF FF FF FF 00 00 00 00 00 00 00 01

nonce = early_data_iv XOR counter:
        4C 3D 2E 1F 00 F1 E2 D3 C4 B5 A6 97
      ⊕ FF FF FF FF 00 00 00 00 00 00 00 01
      = B3 C2 D1 E0 00 F1 E2 D3 C4 B5 A6 96

11. key_phase Flag Test Vector

11.1 key_phase Semantics (Section 9.3)

Flags byte layout:
  Bit 0: control_frame (0 = data, 1 = control)
  Bit 1: RESERVED
  Bit 2: key_phase (0 = current epoch, 1 = next epoch)
  Bits 3-7: RESERVED

key_phase values:
  key_phase=0 (flags & 0x04 == 0): Use current epoch keys
  key_phase=1 (flags & 0x04 == 1): Use next epoch keys (TRANSITION only)

11.2 STEADY State Tests

State: STEADY (only current epoch keys available)

Test Case 1: key_phase=0, data frame
  flags: 0x00
  Expected: Decrypt with current epoch keys → SUCCESS

Test Case 2: key_phase=1, data frame (INVALID in STEADY)
  flags: 0x04
  Expected: REJECT (key_phase=1 invalid when not in TRANSITION)

Test Case 3: key_phase=0, control frame
  flags: 0x01
  Expected: Decrypt with current epoch keys → SUCCESS

11.3 TRANSITION State Tests

State: TRANSITION (both epoch N and epoch N+1 keys available)
Prerequisites: CTRL_REKEY received, epoch N+1 armed

Test Case 4: key_phase=0, epoch N packet
  flags: 0x00
  EncryptedHeader.epoch_id: N
  Expected: Decrypt with epoch N keys → SUCCESS

Test Case 5: key_phase=1, epoch N+1 packet
  flags: 0x04
  EncryptedHeader.epoch_id: N+1
  Expected: Decrypt with epoch N+1 keys → SUCCESS

Test Case 6: key_phase=0, but packet encrypted with epoch N+1 keys
  flags: 0x00 (indicates current epoch)
  Packet actually encrypted with epoch N+1
  Expected: First attempt (epoch N) fails, fallback to epoch N+1 → SUCCESS
  Note: Maximum 2 decrypt attempts per packet

Test Case 7: key_phase=1, but packet encrypted with epoch N keys
  flags: 0x04 (indicates next epoch)
  Packet actually encrypted with epoch N
  Expected: First attempt (epoch N+1) fails, fallback to epoch N → SUCCESS

11.4 key_phase Security Constraints

key_phase MUST NOT trigger:
  - Epoch switches (only CTRL_REKEY can arm epochs)
  - Larger replay windows
  - Owner-backend policy bypass
  - Rate limiting changes

key_phase is a hint for key selection only.
Attacker can flip key_phase bits, but result is:
  - One failed decrypt (if wrong) → packet dropped
  - No amplification, no state changes

12. Epoch Overlap Test Vector

12.1 Epoch Overlap Configuration

Per Section 5 of Anti-Replay Appendix:

Negotiated overlap parameters:
  overlap_duration_ms:      5000   ; 5 seconds
  max_out_of_order_packets: 100

12.2 Epoch Overlap State

During TRANSITION state (epoch overlap active):
  - Both epoch N and epoch N+1 keys are valid
  - Independent replay windows for each epoch
  - Packets from either epoch are accepted (within overlap window)

12.3 Epoch Overlap Test Sequence

Timeline:
  T+0ms:    CTRL_REKEY received, epoch 1 armed, TRANSITION state entered
  T+100ms:  Receive (epoch=1, seq=0) → ACCEPT
  T+200ms:  Receive (epoch=0, seq=1000) → ACCEPT (epoch 0 still valid)
  T+300ms:  Receive (epoch=1, seq=1) → ACCEPT
  T+400ms:  Receive (epoch=0, seq=1001) → ACCEPT (within overlap)
  T+5001ms: Overlap expires, epoch 0 keys discarded
  T+5100ms: Receive (epoch=0, seq=1002) → REJECT (epoch 0 expired)

12.4 Independent Replay Windows

During overlap, maintain SEPARATE replay windows:

Epoch 0 replay window:
  last_seen_seq: 1001
  bitmap: [tracks epoch 0 packets]

Epoch 1 replay window:
  last_seen_seq: 1
  bitmap: [tracks epoch 1 packets]

Test Case: Replay during overlap
  T+500ms: Receive (epoch=0, seq=1000) [REPLAY]
  Expected: REJECT (already seen in epoch 0 window)

  T+600ms: Receive (epoch=1, seq=0) [REPLAY]
  Expected: REJECT (already seen in epoch 1 window)

13. Epoch Transition Test Vector

13.1 Epoch 1 Secret Derivation

epoch_1_secret = HKDF-Expand(
    PRK  = epoch_0_secret,
    info = label("epoch step"),
    L    = 32
)

  epoch_0_secret:
           3C 2D 1E 0F F0 E1 D2 C3 B4 A5 96 87 78 69 5A 4B
           3C 2D 1E 0F F0 E1 D2 C3 B4 A5 96 87 78 69 5A 4B

  info: "PALISADE epoch step"
      = 50 41 4C 49 53 41 44 45 20 65 70 6F 63 68 20 73
        74 65 70

  epoch_1_secret:
           2B 1C 0D FE EF D0 C1 B2 A3 94 85 76 67 58 49 3A
           2B 1C 0D FE EF D0 C1 B2 A3 94 85 76 67 58 49 3A

13.2 Epoch 1 Traffic Keys

c2s_key_1 = HKDF-Expand(epoch_1_secret, label("c2s key"), 32)
  c2s_key_1:
           1A 0B FC ED DE CF B0 A1 92 83 74 65 56 47 38 29
           1A 0B FC ED DE CF B0 A1 92 83 74 65 56 47 38 29

c2s_iv_1 = HKDF-Expand(epoch_1_secret, label("c2s iv"), 12)
  c2s_iv_1:
           09 FA EB DC CD BE AF 90 81 72 63 54

s2c_key_1 = HKDF-Expand(epoch_1_secret, label("s2c key"), 32)
  s2c_key_1:
           F8 E9 DA CB BC AD 9E 8F 70 61 52 43 34 25 16 07
           F8 E9 DA CB BC AD 9E 8F 70 61 52 43 34 25 16 07

s2c_iv_1 = HKDF-Expand(epoch_1_secret, label("s2c iv"), 12)
  s2c_iv_1:
           E7 D8 C9 BA AB 9C 8D 7E 6F 50 41 32

13.3 First Packet of Epoch 1

EncryptedHeader (epoch 1, FIRST packet):
  epoch_id:     00 00 00 01           ; epoch 1
  seq:          00 00 00 00 00 00 00 00 ; sequence 0 (RESET on epoch transition!)
  padding_len:  00 00
  stream_id:    00
  hdr_flags:    00

Nonce construction:
  c2s_iv_1:     09 FA EB DC CD BE AF 90 81 72 63 54
  counter:      00 00 00 01 00 00 00 00 00 00 00 00
  nonce:        09 FA EB DD CD BE AF 90 81 72 63 54
                         ^^ XOR with 01

14. Control Frame Test Vector (CTRL_REKEY)

14.1 CTRL_REKEY Frame

ControlFrame structure:
  ctrl_type:    01                    ; CTRL_REKEY (1 byte)
  length:       00 00 00              ; 0 bytes (3 bytes, big-endian u24, length of ctrl_data only)
  ctrl_data:    (empty)

Encrypted as part of packet payload with:
  - flags bit 0 = 1 (control_frame)
  - Normal AEAD encryption

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).

14.2 PublicHeader for Control Frame

51 50                             ; magic "QP"
12                                ; version 0x12
01                                ; flags (control=1, key_phase=0)
00 42                             ; length (66 bytes total: 30 header + 20 ciphertext + 16 tag)
[24 bytes RID]

Note: Control frame packets have a minimum size of 66 bytes because the ControlFrame structure
(ctrl_type + length = 4 bytes) is part of the encrypted payload, even when ctrl_data is empty.
The plaintext is: EncryptedHeader (16 bytes) + ControlFrame header (4 bytes) = 20 bytes,
which encrypts to 20 bytes ciphertext + 16 bytes tag = 36 bytes, plus 30 bytes PublicHeader = 66 bytes total.

14.3 CTRL_REKEY Processing

On receiving valid CTRL_REKEY:
  1. Derive epoch_(N+1)_secret from epoch_N_secret
  2. Derive epoch_(N+1) traffic keys
  3. ARM epoch N+1 (do not PROMOTE yet)
  4. Enter TRANSITION state
  5. Accept packets from both epoch N and N+1
  6. PROMOTE to epoch N+1 only after successful decrypt with N+1 keys

15. Reserved Flag Bit Test

15.1 Invalid Packets (Must Reject)

Test Case: Reserved bit 1 set
  flags: 02                         ; bit 1 set (reserved)
  Expected: Packet dropped silently

Test Case: Reserved bits 3-7 set
  flags: F8                         ; bits 3-7 set
  Expected: Packet dropped silently

Test Case: All reserved bits set
  flags: FA                         ; bits 1, 3-7 set
  Expected: Packet dropped silently

15.2 Valid Flag Combinations

flags: 00  ; data frame, key_phase=0
flags: 01  ; control frame, key_phase=0
flags: 04  ; data frame, key_phase=1
flags: 05  ; control frame, key_phase=1

16. Security Invariant Verification Points

16.1 Handshake Invariants

InvariantVerification
Transcript bindingVerify transcript_hash includes all CH/SH fields
Version in transcriptConfirm version included before HKDF
Failed handshake → no keysVerify key wipe on any failure

16.2 Nonce Invariants

InvariantVerification
(epoch, seq) uniquenessSequence increments atomically
No (epoch, seq) reuseReplay window rejects duplicates
Epoch monotonicityepoch_id never decreases
Sequence starts at 0First packet of each epoch has seq=0

16.3 Replay Invariants

InvariantVerification
Window-based rejectionPackets outside window rejected
Epoch-gated promotionNo promote without CTRL_REKEY
2-attempt limitMax 2 decrypt attempts per packet
Independent epoch windowsSeparate replay windows during overlap

16.4 Early Data Invariants

InvariantVerification
Reserved epochEarly data uses epoch 0xFFFFFFFF
seq >= 1Early data sequence starts at 1, not 0
key_phase=0Early data must have key_phase=0
Idempotent onlyEarly data operations must be replayable

17. Implementation Checklist

  • ClientHello serialization matches Section 3
  • ServerHello serialization matches Section 4
  • Transcript excludes server_signature, dos_cookie, padding
  • Key schedule matches Section 5 derivation order
  • HKDF labels include "PALISADE " prefix
  • Nonce construction uses XOR of full 12-byte counter
  • PublicHeader is 30 bytes with 24-byte RID
  • EncryptedHeader is 16 bytes with hdr_flags=0x00
  • Reserved flag bits (1, 3-7) cause packet drop
  • Replay window uses (epoch, seq) only
  • Epoch promotion requires authenticated CTRL_REKEY
  • Maximum 2 decrypt attempts per packet
  • Sequence starts at 0 for each epoch (not 1)
  • Sequence resets to 0 on epoch transition (atomic)
  • Early data uses epoch 0xFFFFFFFF with seq >= 1
  • key_phase=1 only valid during TRANSITION state
  • Epoch overlap maintains independent replay windows

Appendix A: HKDF Label Reference

Label StringHex Encoding
PALISADE handshake secret50 41 4C 49 53 41 44 45 20 68 61 6E 64 73 68 61 6B 65 20 73 65 63 72 65 74
PALISADE master secret50 41 4C 49 53 41 44 45 20 6D 61 73 74 65 72 20 73 65 63 72 65 74
PALISADE epoch 050 41 4C 49 53 41 44 45 20 65 70 6F 63 68 20 30
PALISADE epoch step50 41 4C 49 53 41 44 45 20 65 70 6F 63 68 20 73 74 65 70
PALISADE c2s key50 41 4C 49 53 41 44 45 20 63 32 73 20 6B 65 79
PALISADE c2s iv50 41 4C 49 53 41 44 45 20 63 32 73 20 69 76
PALISADE s2c key50 41 4C 49 53 41 44 45 20 73 32 63 20 6B 65 79
PALISADE s2c iv50 41 4C 49 53 41 44 45 20 73 32 63 20 69 76
PALISADE early data key50 41 4C 49 53 41 44 45 20 65 61 72 6C 79 20 64 61 74 61 20 6B 65 79
PALISADE early data iv50 41 4C 49 53 41 44 45 20 65 61 72 6C 79 20 64 61 74 61 20 69 76
PALISADE ticket secret50 41 4C 49 53 41 44 45 20 74 69 63 6B 65 74 20 73 65 63 72 65 74
PALISADE resumption psk50 41 4C 49 53 41 44 45 20 72 65 73 75 6D 70 74 69 6F 6E 20 70 73 6B
PALISADE rid steering50 41 4C 49 53 41 44 45 20 72 69 64 20 73 74 65 65 72 69 6E 67
PALISADE rid privacy50 41 4C 49 53 41 44 45 20 72 69 64 20 70 72 69 76 61 63 79

Appendix B: Error Conditions

ConditionExpected Behavior
Magic ≠ 0x5150Drop packet silently
Version ≠ 0x12Send version mismatch error
Reserved flags setDrop packet silently
Packet < 62 bytes (data) or < 66 bytes (control)Drop packet silently
Packet > MTUDrop packet silently
AEAD decryption failsDrop packet silently
Replay detectedDrop packet silently
Epoch jump without CTRL_REKEYDrop packet silently
hdr_flags ≠ 0x00Drop packet silently
Nonce reuse detectedTerminate session, log CRITICAL
key_phase=1 in STEADY stateDrop packet silently
Early data with seq=0Drop packet silently
Early data with key_phase=1Drop packet silently

Appendix C: Test Vector Summary

SectionTest Coverage
5Key schedule derivation with deterministic outputs
6Traffic key derivation with full key values
7Nonce construction (seq=0 first packet)
8Complete encrypted packet example
9Replay protection (seq starts at 0)
10Early data (epoch 0xFFFFFFFF, seq >= 1)
11key_phase flag behavior
12Epoch overlap with independent windows
13Epoch transition (seq resets to 0)
14Control frame format
15Reserved flag rejection

PALISADE Protocol Specification Draft 00

INFORMATIONAL