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 concatenationXORor⊕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 741.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 4B6. 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 FE8. 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 FF8.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 909. 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 unchanged10. 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 9710.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
| Constraint | Requirement |
|---|---|
| key_phase | MUST be 0 (Section 12.8) |
| epoch_id | MUST be 0xFFFFFFFF |
| seq | MUST be >= 1 (seq=0 reserved) |
| Replay | Separate replay window from regular traffic |
| Operations | MUST 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 9611. 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 3A13.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 3213.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 0114. 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
| Invariant | Verification |
|---|---|
| Transcript binding | Verify transcript_hash includes all CH/SH fields |
| Version in transcript | Confirm version included before HKDF |
| Failed handshake → no keys | Verify key wipe on any failure |
16.2 Nonce Invariants
| Invariant | Verification |
|---|---|
| (epoch, seq) uniqueness | Sequence increments atomically |
| No (epoch, seq) reuse | Replay window rejects duplicates |
| Epoch monotonicity | epoch_id never decreases |
| Sequence starts at 0 | First packet of each epoch has seq=0 |
16.3 Replay Invariants
| Invariant | Verification |
|---|---|
| Window-based rejection | Packets outside window rejected |
| Epoch-gated promotion | No promote without CTRL_REKEY |
| 2-attempt limit | Max 2 decrypt attempts per packet |
| Independent epoch windows | Separate replay windows during overlap |
16.4 Early Data Invariants
| Invariant | Verification |
|---|---|
| Reserved epoch | Early data uses epoch 0xFFFFFFFF |
| seq >= 1 | Early data sequence starts at 1, not 0 |
| key_phase=0 | Early data must have key_phase=0 |
| Idempotent only | Early 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 String | Hex Encoding |
|---|---|
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 |
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 |
PALISADE epoch 0 | 50 41 4C 49 53 41 44 45 20 65 70 6F 63 68 20 30 |
PALISADE epoch step | 50 41 4C 49 53 41 44 45 20 65 70 6F 63 68 20 73 74 65 70 |
PALISADE c2s key | 50 41 4C 49 53 41 44 45 20 63 32 73 20 6B 65 79 |
PALISADE c2s iv | 50 41 4C 49 53 41 44 45 20 63 32 73 20 69 76 |
PALISADE s2c key | 50 41 4C 49 53 41 44 45 20 73 32 63 20 6B 65 79 |
PALISADE s2c iv | 50 41 4C 49 53 41 44 45 20 73 32 63 20 69 76 |
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 |
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 |
PALISADE ticket secret | 50 41 4C 49 53 41 44 45 20 74 69 63 6B 65 74 20 73 65 63 72 65 74 |
PALISADE resumption psk | 50 41 4C 49 53 41 44 45 20 72 65 73 75 6D 70 74 69 6F 6E 20 70 73 6B |
PALISADE rid steering | 50 41 4C 49 53 41 44 45 20 72 69 64 20 73 74 65 65 72 69 6E 67 |
PALISADE rid privacy | 50 41 4C 49 53 41 44 45 20 72 69 64 20 70 72 69 76 61 63 79 |
Appendix B: Error Conditions
| Condition | Expected Behavior |
|---|---|
| Magic ≠ 0x5150 | Drop packet silently |
| Version ≠ 0x12 | Send version mismatch error |
| Reserved flags set | Drop packet silently |
| Packet < 62 bytes (data) or < 66 bytes (control) | Drop packet silently |
| Packet > MTU | Drop packet silently |
| AEAD decryption fails | Drop packet silently |
| Replay detected | Drop packet silently |
| Epoch jump without CTRL_REKEY | Drop packet silently |
| hdr_flags ≠ 0x00 | Drop packet silently |
| Nonce reuse detected | Terminate session, log CRITICAL |
| key_phase=1 in STEADY state | Drop packet silently |
| Early data with seq=0 | Drop packet silently |
| Early data with key_phase=1 | Drop packet silently |
Appendix C: Test Vector Summary
| Section | Test Coverage |
|---|---|
| 5 | Key schedule derivation with deterministic outputs |
| 6 | Traffic key derivation with full key values |
| 7 | Nonce construction (seq=0 first packet) |
| 8 | Complete encrypted packet example |
| 9 | Replay protection (seq starts at 0) |
| 10 | Early data (epoch 0xFFFFFFFF, seq >= 1) |
| 11 | key_phase flag behavior |
| 12 | Epoch overlap with independent windows |
| 13 | Epoch transition (seq resets to 0) |
| 14 | Control frame format |
| 15 | Reserved flag rejection |
PALISADE Protocol Specification Draft 00
INFORMATIONAL