PALISADE v1.2 Negative Test Vectors
This document defines negative test vectors for PALISADE v1.2, ensuring implementations correctly reject invalid inputs and maintain fail-closed behavior. These vectors complement the positive end-to-end test vector in e2e.md.
Common Test Parameters
All negative vectors use the same base parameters as the positive E2E vector:
| Component | Value |
|---|---|
| Protocol Version | 0x12 (PALISADE v1.2) |
| KEM | ML-KEM-768 (0x0011) |
| Signature | ML-DSA-65 (0x0021) |
| AEAD | ChaCha20-Poly1305 (0x0001) |
| Hash | SHA3-256 |
| HKDF | HKDF-SHA3-256 |
| PublicHeader | 30 bytes (magic + version + flags + length + RID) |
| RID | 24 bytes (8 steering + 2 epoch + 14 privacy) |
WARNING
This document contains deterministic cryptographic material for testing only. DO NOT USE THESE VALUES IN PRODUCTION.
Error Code Policy
Error codes referenced in this document (e.g., ErrorPacketTooLarge 0x0305) are semantic identifiers for internal logging, counters, and telemetry. On-wire error responses are only sent where the specification explicitly allows them (e.g., during handshake for ErrorInvalidVersion). For data plane errors, implementations MUST drop packets silently and MAY increment the corresponding error counter for monitoring purposes.
1. Handshake Tampering / Transcript Hash Mismatch
1.1 Purpose
This vector ensures implementations:
- Reject invalid handshakes where the transcript hash does not match the signature
- Fail closed without installing traffic keys
- Transition to ABORT/CLOSED state on signature verification failure
1.2 Normative Requirements
Implementations MUST:
- Reject the handshake at the signature verification step
- NOT install application keys or accept application packets
- Transition to an ABORT/CLOSED terminal state
- Zeroize any ephemeral secrets already derived
1.3 Baseline (Correct) Values
From the positive E2E vector, using PALISADE v1.2 wire format:
ClientHello: version: 0x12 ; single byte, PALISADE v1.2 client_nonce: 01 02 03 ... 20 ; 32 bytes K_c: [1184 bytes] ; ML-KEM-768 public key (actual size) ServerHello: version: 0x12 ; single byte, negotiated v1.2 server_nonce: A1 A2 A3 ... C0 ; 32 bytes CT_c: [1088 bytes] ; ML-KEM-768 ciphertext (actual size) server_signature: [3293 bytes] ; ML-DSA-65 signature over transcript_hash
The correct transcript hash:
transcript_hash_correct = SHA3-256(canonical_ClientHello || canonical_ServerHello_without_signature)
Canonical Encoding Rules
(per End-To-End Test Vector Section 3):
- For transcript hashing, only field contents are included; length prefixes are not hashed
- Fields are concatenated in wire order
- No padding or alignment bytes
Note: For complete field definitions and canonical byte encodings, see End-To-End Test Vector Sections 3-4.
1.4 Injected Fault
An attacker modifies one bit in the KEM ciphertext CT_c in transit:
CT_c'[0] = CT_c[0] XOR 0x01 CT_c'[1..] = CT_c[1..]
All other bytes are unchanged, including:
- Server certificate
server_signature(unchanged, still signs the original transcript)
This creates a transcript mismatch because ServerHello_without_signature is now different.
1.5 Resulting Transcript Hash
The client computes:
transcript_hash_bad = SHA3-256(canonical_ClientHello || canonical_ServerHello_with_CT_c')
By construction: transcript_hash_bad != transcript_hash_correct
1.6 Required Client Behavior
Verification Step
During signature verification, the client MUST verify:
Verify(server_public_key, transcript_hash_bad, server_signature)
This verification MUST FAIL because server_signature is a signature over transcript_hash_correct, not transcript_hash_bad.
Failure Handling
Upon signature verification failure, the client MUST:
- Abort the handshake immediately
- Transition to ABORT/CLOSED state
- Zeroize any ephemeral secrets (including KEM decapsulation outputs)
- NOT proceed to HKDF key derivation
- NOT accept or emit any application-data packets
Error Code: ErrorSignatureVerifyFailed (0x0106)
1.7 Required Server Behavior
The server may remain unaware of the failure (tampering is indistinguishable from packet loss). However, if the server receives:
- An invalid
HandshakeFinished, or - Any out-of-context application packet
The server MUST reject and close the connection.
1.8 Validation Checklist
An implementation passes if:
- Signature verification fails at the expected step
- No traffic keys are installed
- Connection transitions to ABORT/CLOSED
- Any subsequent application packet is rejected
- Key material is zeroized
2. Application Packet Replay
2.1 Purpose
This vector ensures implementations:
- Enforce replay protection using
(epoch_id, seq)pairs - Reject duplicate packets even if they decrypt correctly
- Accept sequence 0 for regular packets (only early data requires seq >= 1)
- Maintain correct state under adversarial packet duplication
2.2 Normative Requirements
Implementations MUST:
- Reject any packet whose
(epoch_id, seq)has already been accepted - Reject replays even if the packet is otherwise valid and authenticates correctly
- Accept sequence 0 for regular packets (not early data)
- NOT deliver replayed payloads to the application layer
2.3 Preconditions
A successful handshake has completed using the positive E2E vector:
- Current epoch:
epoch_id = 0x00000000 - Replay window: Initialized, accepts sequence 0 and above
- State: ESTABLISHED
2.4 Test Case A: First Packet with Sequence 0
Verify that sequence 0 is valid for regular packets. This test uses the canonical 78-byte packet format from End-To-End Test Vector Section 8.5.
PublicHeader (30 bytes)
51 50 ; magic "QP" 12 ; version 0x12 00 ; flags (data, key_phase=0) 00 4E ; length = 78 (30 header + 32 ciphertext + 16 tag) [24 bytes RID] ; Routing Identifier
EncryptedHeader (16 bytes plaintext, before encryption)
epoch_id: 00 00 00 00 ; epoch 0 seq: 00 00 00 00 00 00 00 00 ; sequence 0 (VALID) padding_len: 00 00 stream_id: 00 hdr_flags: 00
Payload: 16 bytes (e.g., "Hello PALISADE!")
Wire Format: 78 bytes total = 30 (PublicHeader) + 32 (ciphertext: 16 EncryptedHeader + 16 payload) + 16 (AEAD tag)
Expected Behavior: ACCEPT (sequence 0 is valid for regular packets)
2.5 Test Case B: Replay Attack
Packet P: First packet with (epoch=0, seq=0)
Transmission Sequence
- Deliver
P(original) → ACCEPT - Deliver
Pagain (replay) → REJECT
No bytes are modified between deliveries.
2.6 Required Receiver Behavior
First Delivery (ACCEPT)
- Validate and parse the PublicHeader
- Verify
(epoch_id=0, seq=0)is within window and not previously seen - Perform AEAD decryption/verification
- Deliver payload to application
- Mark
(epoch_id=0, seq=0)as seen/accepted
Second Delivery (REJECT)
- Detect that
(epoch_id=0, seq=0)has already been accepted - Reject the packet before delivering decrypted payload
The receiver MAY:
- Drop silently, or
- Emit
ErrorReplayDetected(0x0303) diagnostic counter/log
The receiver MUST NOT:
- Deliver payload a second time
- Advance any application-level state based on the replay
- Trigger rekey/migration based on replayed packets
Rejection Point
To minimize CPU usage and DoS risk, reject replays:
- Prior to AEAD decryption when possible (based on
(epoch_id, seq)tracking) - Otherwise immediately after header validation
2.7 Extended Test: Out-of-Order with Replay
Test that out-of-order acceptance does not break replay tracking:
Delivery Sequence
- Packet with
(epoch=0, seq=2)→ ACCEPT - Packet with
(epoch=0, seq=0)→ ACCEPT (reordered, within window) - Packet with
(epoch=0, seq=1)→ ACCEPT (reordered, within window) - Replay of
(epoch=0, seq=0)→ REJECT
2.8 Validation Checklist
An implementation passes if:
- Sequence 0 is accepted for the first packet
- Replay of any
(epoch, seq)pair is rejected - Replay detection does not depend on timestamp presence
(epoch_id, seq)tracking remains correct under reordering- No state corruption occurs from replayed packets
3. Length-Field Overflow / Size Cap Violation
3.1 Purpose
This vector ensures implementations:
- Enforce packet size limits before allocation or decryption
- Reject packets exceeding MTU (default 1500 bytes)
- Reject packets below minimum size (62 bytes)
- Fail fast without buffer overflow or resource exhaustion
3.2 Normative Requirements
Per Section 8.5 of the specification:
Implementations MUST:
- Reject packets with total length < 62 bytes (MIN_PACKET_SIZE)
- Reject packets with total length > configured MTU (default 1500 bytes)
- Perform size checks before AEAD decryption
- Drop violating packets silently (no error response)
Minimum Size Calculation
MIN_PACKET_SIZE = PublicHeader (30) + EncryptedHeader (16) + AEAD tag (16) = 62 bytes MIN_CONTROL_SIZE = PublicHeader (30) + EncryptedHeader (16) + ControlFrame header (4) + AEAD tag (16) = 66 bytes
Note: EncryptedHeader (16 bytes plaintext) is encrypted. ControlFrame header is 4 bytes (1 ctrl_type + 3 length).
3.3 Negative Case A: Packet Too Small
Packet Construction
Total packet size: 61 bytes (one byte under minimum) PublicHeader (30 bytes): 51 50 ; magic 12 ; version 00 ; flags 00 3D ; length = 61 [24 bytes RID] Remaining: 31 bytes (should be at least 32: 16 ciphertext + 16 tag)
Required Behavior
- Reject immediately after parsing PublicHeader
- Do NOT attempt AEAD decryption
- Drop silently (no error response)
3.4 Negative Case B: Packet Exceeds MTU
Packet Construction
Total packet size: 1501 bytes (one byte over default MTU) PublicHeader (30 bytes): 51 50 ; magic 12 ; version 00 ; flags 05 DD ; length = 1501 [24 bytes RID] Remaining: 1471 bytes of payload/padding
Required Behavior
- Reject immediately after parsing length field
- Do NOT attempt to read full payload
- Do NOT attempt AEAD decryption
- Drop silently
- Error:
ErrorPacketTooLarge(0x0305)
3.5 Negative Case C: Length Field Mismatch
Packet Construction
Actual UDP datagram size: 100 bytes PublicHeader claims: length = 0x0FFF (4095 bytes)
Required Behavior
- Detect mismatch between claimed length and actual datagram size
- Reject packet
- Do NOT attempt to read beyond datagram bounds
3.6 Negative Case D: Reserved Flag Bits Set
Per Section 8.1.1, reserved flag bits (1, 3-7) must be zero.
Packet Construction
PublicHeader (30 bytes): 51 50 ; magic 12 ; version FA ; flags = 0xFA (reserved bits 1, 3-7 set) 00 50 ; length = 80 [24 bytes RID] [Valid ciphertext follows]
Required Behavior
- Check
(flags & 0xFA) != 0→ TRUE - Reject packet immediately
- Do NOT attempt AEAD decryption
- Drop silently
3.7 Negative Case E: hdr_flags Non-Zero (Post-Decryption)
Per Section 8.2, hdr_flags in EncryptedHeader must be 0x00.
Packet Construction
After successful AEAD decryption, EncryptedHeader contains:
epoch_id: 00 00 00 00 seq: 00 00 00 00 00 00 00 00 padding_len: 00 00 stream_id: 00 hdr_flags: 01 ; INVALID (must be 0x00)
Required Behavior
- After successful AEAD decryption, validate
hdr_flags == 0x00 - Reject packet if
hdr_flags != 0x00 - Do NOT deliver payload to application
3.8 Negative Case F: AEAD Authentication Failure
This test explicitly verifies AEAD decryption failure handling (currently implicit in other tests).
Packet Construction
(78 bytes, canonical format):
PublicHeader (30 bytes): 51 50 ; magic 12 ; version 00 ; flags (valid) 00 4E ; length = 78 (valid) [24 bytes RID] ; valid RID Ciphertext (32 bytes): [valid-looking ciphertext] Tag (16 bytes): DE AD BE EF CA FE BA BE ; CORRUPTED: 1 bit flipped from valid tag 12 34 56 78 9A BC DE F0
Required Behavior
- Attempt AEAD decryption (all pre-checks pass)
- AEAD authentication fails due to corrupted tag
- Drop silently
- MAY increment counter
ErrorAEADDecryptFailed(0x0300) - Do NOT deliver any plaintext to application
- Do NOT modify session state
3.9 Negative Case G: padding_len Overflow (Post-Decryption)
This tests a classic parser bug: padding_len claiming more bytes than available.
Packet Construction
(78 bytes total):
After successful AEAD decryption, plaintext (32 bytes) contains:
EncryptedHeader (16 bytes): epoch_id: 00 00 00 00 seq: 00 00 00 00 00 00 00 01 padding_len: 00 20 ; INVALID: claims 32 bytes padding stream_id: 00 hdr_flags: 00 Payload (16 bytes): [16 bytes of application data]
Condition: padding_len (32) > available payload bytes (16)
Required Behavior
- AEAD decryption succeeds
- Parse EncryptedHeader, extract
padding_len = 32 - Detect:
padding_len > (plaintext_len - 16)→ INVALID - Reject packet post-decryption
- Do NOT deliver payload to application
- Do NOT crash or read out of bounds
- Drop silently
3.10 Negative Case H: stream_id Validation (STREAM_DUMMY)
Per Section 8.2, stream_id = 0xFF is reserved as STREAM_DUMMY.
Packet Construction
After successful AEAD decryption, EncryptedHeader contains:
epoch_id: 00 00 00 00 seq: 00 00 00 00 00 00 00 01 padding_len: 00 00 stream_id: FF ; STREAM_DUMMY (reserved) hdr_flags: 00
Required Behavior
- AEAD decryption succeeds
- Parse EncryptedHeader, extract
stream_id = 0xFF - Recognize as STREAM_DUMMY (reserved for padding/dummy traffic)
- Do NOT deliver payload to application layer
- Do NOT treat as error (this is valid wire format for dummy traffic)
- MAY use for traffic shaping/padding purposes
Note: STREAM_DUMMY is not an error condition—it's a valid mechanism for traffic shaping. This test verifies implementations recognize it and do NOT deliver to application.
Important: STREAM_DUMMY packets still consume replay window entries and sequence numbers. They are not "out of band" traffic—they are fully validated packets that simply have no application payload to deliver.
3.11 Validation Checklist
An implementation passes if:
- Packets < 62 bytes are rejected before decryption
- Packets > MTU are rejected before decryption
- Length field mismatches are detected and rejected
- Reserved flag bits cause immediate rejection
hdr_flags != 0x00causes post-decryption rejection- AEAD authentication failure causes silent drop
padding_lenoverflow is detected post-decryptionstream_id = 0xFF(STREAM_DUMMY) is not delivered to application- No buffer overflow, over-read, or unbounded allocation occurs
- Implementation remains stable under repeated invalid inputs
4. Early Data Limit Violation
4.1 Purpose
This vector ensures implementations:
- Enforce
max_early_data_bytesfrom the resumption ticket, not from client offer - Reject excess 0-RTT data
- Reject early data outside the early-data phase
- Treat early data as replayable and non-authoritative
4.2 Normative Requirements
Per the specification and security invariants:
Implementations MUST:
- Enforce
max_early_data_bytesfrom the ticket (authoritative) - Ignore any client-offered early data limits (non-authoritative)
- Reject early data that exceeds the ticket-defined limit
- Reject early data packets received outside the early-data phase
- NOT allow early data to trigger epoch transitions, rekeys, or migrations
4.3 Preconditions
Ticket Contents (Authoritative)
Resumption Ticket T: max_early_data_bytes = 1024 ticket_id = <opaque> allowed_operations = [idempotent only]
The value 1024 is authoritative and MUST be enforced.
ClientHelloResume Offer (Non-Authoritative)
Client claims: client_offered_max_early_data_bytes = 65535
This value MUST be ignored by the server.
4.4 Negative Case A: Early Data Exceeds Ticket Limit
Client Behavior (Input to System Under Test)
- Client sends
ClientHelloResumereferencing ticketT - Client immediately sends 0-RTT data totaling 1025 bytes:
- Early Data chunk 1: 800 bytes
- Early Data chunk 2: 225 bytes
- Total: 1025 bytes (one byte over limit)
4.5 Required Server Behavior
Enforcement Rule
early_data_accepted_bytes <= ticket.max_early_data_bytes
Server MUST use ticket's max_early_data_bytes = 1024, ignoring client value.
Rejection Condition
As soon as cumulative early data exceeds 1024 bytes:
- Reject early data
- Ensure no bytes beyond the limit are processed as application input
Concretely:
- Bytes 0..1023 MAY be accepted if early data is permitted by policy
- Byte 1024 (the 1025th byte) MUST NOT be processed
- Server MUST signal rejection via
ErrorEarlyDataRejected(0x0403)
Continuation Modes
Mode 1 (Recommended): Reject Early Data, Continue in 1-RTT
- Discard all early data (or at minimum, bytes beyond limit)
- Signal rejection via
early_data_rejectedindicator - Complete resumption handshake
- Derive 1-RTT keys
- Accept only 1-RTT application data
Mode 2: Reject Resumption Entirely
- Reject resumption (
ResumeReject) - Discard all early data
- Require full handshake
4.6 Negative Case B: Early Data Outside Phase
Scenario: Client sends early data packet after resumption handshake completes.
State: Resumption handshake completed, early-data phase ended, session ESTABLISHED
Packet
EncryptedHeader: epoch_id: FF FF FF FF ; Reserved early data epoch seq: 00 00 00 00 00 00 00 01 ...
Required Behavior
- REJECT (early data epoch invalid outside early-data phase)
- Do NOT decrypt or process the packet
- Drop silently or emit
ErrorInvalidEpoch(0x0301)
4.7 Negative Case C: Early Data with Sequence 0
Per Section 12.8, early data requires seq >= 1.
Packet
EncryptedHeader: epoch_id: FF FF FF FF ; Reserved early data epoch seq: 00 00 00 00 00 00 00 00 ; seq = 0 (INVALID for early data) ...
Required Behavior
- REJECT (early data must have seq >= 1)
- Drop silently
4.8 Security Invariants for Early Data
Per the Security Invariants appendix:
| Invariant | Requirement |
|---|---|
| Replayable data cannot steer state | Early data MUST NOT cause durable state transitions |
| Non-authoritative | Early data limits from ticket only, not client |
| Idempotent operations only | Early data suitable only for idempotent operations |
| Isolated keys | Early data keys cryptographically separated from 1-RTT keys |
4.9 Validation Checklist
An implementation passes if:
- Server enforces ticket-defined limit (1024) regardless of client offer
- Server rejects over-limit early data
- Server does not process the 1025th byte
- Server rejects early data outside the early-data phase
- Server rejects early data with seq = 0
- Server signals rejection consistently
- No "silent acceptance" occurs
- Early data does not trigger epoch/migration/rekey state changes
5. Invalid Magic Byte
5.1 Purpose
This vector ensures implementations:
- Reject packets with invalid magic bytes as the first line of defense
- Do not process non-PALISADE packets
- Fail fast before any further parsing
5.2 Normative Requirements
Per Section 8.1.1:
Magic = 0x5150 ("QP") - packets with magic ≠ 0x5150 MUST be rejected immediately.
5.3 Test Cases
Test Case A: All Zeros
PublicHeader[0:2] = 00 00 (invalid) Expected: Reject immediately, drop silently
Test Case B: All Ones
PublicHeader[0:2] = FF FF (invalid) Expected: Reject immediately, drop silently
Test Case C: Reversed Magic
PublicHeader[0:2] = 50 51 (reversed "PQ") Expected: Reject immediately, drop silently
Test Case D: One Byte Wrong
PublicHeader[0:2] = 51 51 (one byte wrong) Expected: Reject immediately, drop silently
5.4 Required Behavior
Upon detecting invalid magic bytes:
- Reject immediately (first 2 bytes of packet)
- Do NOT parse any further fields
- Do NOT attempt AEAD decryption
- Drop silently (no error response to potential attacker)
- MAY increment an invalid-packet counter for monitoring
5.5 Validation Checklist
An implementation passes if:
- All invalid magic values are rejected
- Rejection occurs before parsing version/flags/length
- No further processing occurs for invalid magic
- Implementation remains stable under repeated invalid magic packets
6. Invalid Protocol Version
6.1 Purpose
This vector ensures implementations:
- Reject unsupported protocol versions
- Prevent version downgrade attacks
- Handle version mismatch between handshake and data packets
6.2 Normative Requirements
Per Section 8.1.1 and Section 16:
Version = 0x12 for PALISADE v1.2 - unsupported versions MUST cause rejection.
6.3 Test Cases
Test Case A: Version Zero
PublicHeader: 51 50 ; valid magic 00 ; version = 0x00 (INVALID) ... Expected: Reject, emit ErrorInvalidVersion (0x0100)
Test Case B: Future Version
PublicHeader: 51 50 ; valid magic FF ; version = 0xFF (unsupported future version) ... Expected: Reject, emit ErrorInvalidVersion (0x0100)
Test Case C: Version 1.1 (If Not Supported)
PublicHeader:
51 50 ; valid magic
11 ; version = 0x11 (v1.1, may not be supported)
...
Expected: If v1.1 not supported, reject with ErrorInvalidVersion (0x0100)
If v1.1 supported, process according to v1.1 rulesTest Case D: Version Mismatch Between Handshake and Data
State: Handshake completed with version 0x12 Packet: Data packet with version 0x11 Expected: Reject (version mismatch)
6.4 Required Behavior
Upon detecting unsupported version:
- Reject packet
- Emit
ErrorInvalidVersion(0x0100) if in handshake phase - Drop silently if in data phase (to avoid amplification)
- Do NOT attempt AEAD decryption
- Do NOT downgrade to earlier version without explicit negotiation
6.5 Validation Checklist
An implementation passes if:
- Version 0x00 is rejected
- Version 0xFF is rejected
- Unsupported older versions are rejected
- Version mismatch between handshake and data is detected
- No version downgrade occurs without proper negotiation
7. key_phase Flag Validation
7.1 Purpose
This vector ensures implementations:
- Reject
key_phase=1in STEADY state - Reject
key_phase=1on early data packets - Only allow
key_phase=1during TRANSITION state with armed next epoch
7.2 Normative Requirements
Per Section 9.3 and Section 11.11:
key_phase=0: Use current epoch keyskey_phase=1: Use next epoch keys (only valid during TRANSITION state)key_phase=1on early data (epoch 0xFFFFFFFF) is always invalid- Receivers MUST reject
key_phase=1if next epoch is not armed
Decrypt Attempt Limit
Receivers MUST attempt decryption with at most 2 key sets:
- STEADY state: Only current epoch keys (1 attempt max)
- TRANSITION state: Try hinted epoch first (
key_phaseselects), then fallback to other epoch (2 attempts max)
This bounds CPU usage and prevents attackers from causing unbounded decryption attempts.
7.3 Test Case A: key_phase=1 in STEADY State
State: STEADY (epoch 0, no next epoch armed)
Packet
(78 bytes total, canonical format):
PublicHeader (30 bytes): 51 50 ; magic 12 ; version 04 ; flags = 0x04 (key_phase=1, bit 2 set) 00 4E ; length = 78 [24 bytes RID] [32 bytes ciphertext] ; EncryptedHeader + payload [16 bytes AEAD tag]
Expected Behavior
- REJECT immediately
- Do NOT attempt decryption with any keys
- Drop silently
- Reason:
key_phase=1is invalid when no next epoch is armed
7.4 Test Case B: key_phase=1 on Early Data
State: Early data phase active
Packet
PublicHeader: 51 50 ; magic 12 ; version 04 ; flags = 0x04 (key_phase=1) ... EncryptedHeader (after decryption): epoch_id: FF FF FF FF ; Reserved early data epoch seq: 00 00 00 00 00 00 00 01 ...
Expected Behavior
- REJECT (Section 12.8: key_phase=1 invalid for early data)
- Do NOT process the packet
- Drop silently
7.5 Test Case C: key_phase=1 Without Armed Epoch
State: STEADY, no CTRL_REKEY received, epoch 0 only
Packet
PublicHeader: flags = 0x04 (key_phase=1) EncryptedHeader: epoch_id: 00 00 00 01 ; epoch 1 (NOT armed) ...
Expected Behavior
- REJECT (next epoch not armed)
- Do NOT attempt decryption
- Drop silently
7.6 Test Case D: key_phase=1 Valid in TRANSITION State
State: TRANSITION (epoch 0 current, epoch 1 armed via CTRL_REKEY)
Packet
PublicHeader: flags = 0x04 (key_phase=1) EncryptedHeader: epoch_id: 00 00 00 01 ; epoch 1 (armed) ...
Expected Behavior
- Attempt decryption with epoch 1 keys
- If successful, ACCEPT
- Update epoch state as appropriate
7.7 Validation Checklist
An implementation passes if:
key_phase=1in STEADY state is rejectedkey_phase=1on early data is rejectedkey_phase=1without armed next epoch is rejectedkey_phase=1in TRANSITION state with armed epoch succeeds- No key confusion occurs between epochs
8. Invalid Epoch Transitions
8.1 Purpose
This vector ensures implementations:
- Enforce epoch sovereignty (server controls epoch transitions)
- Reject packets from unarmed epochs
- Reject epoch jumps (skipping epochs)
- Reject client unilateral epoch increments
8.2 Normative Requirements
Per Section 11.2 and Section 11.10:
- Clients MUST NOT unilaterally increment epoch
- Epoch transitions MUST occur only via authenticated CTRL_REKEY
- Receivers MUST NOT advance epoch based solely on observing higher epoch_id
- Epoch N+1 must be armed before accepting packets with epoch_id = N+1
Error Handling Policy
- Handshake epoch errors → close session, emit
ErrorInvalidEpoch(0x0301) - Data-plane epoch errors → silent drop, MAY increment
ErrorInvalidEpochcounter
8.3 Test Case A: Unarmed Epoch Jump
State: STEADY, epoch 0 current, no CTRL_REKEY received
Packet
EncryptedHeader: epoch_id: 00 00 00 01 ; epoch 1 (NOT armed) seq: 00 00 00 00 00 00 00 00 ...
Expected Behavior
- REJECT (epoch 1 not armed)
- Emit
ErrorInvalidEpoch(0x0301) - Do NOT attempt decryption with unarmed epoch keys; only try keys that are currently installed/allowed (current and armed-next, max 2 attempts).
- Do NOT arm epoch 1 based on this packet
8.4 Test Case B: Epoch Skip
State: STEADY, epoch 0 current
Packet
EncryptedHeader: epoch_id: 00 00 00 02 ; epoch 2 (skipped epoch 1) seq: 00 00 00 00 00 00 00 00 ...
Expected Behavior
- REJECT (epoch jump, epoch 2 cannot be armed without epoch 1)
- Emit
ErrorInvalidEpoch(0x0301) - Do NOT accept epoch jumps
8.5 Test Case C: Client Unilateral Epoch Increment
State: Client in STEADY, epoch 0
Client Action: Client sends packet with epoch_id = 0x00000001 without receiving CTRL_REKEY from server
Expected Server Behavior
- REJECT (Section 11.2: clients MUST NOT unilaterally increment epoch)
- Emit
ErrorInvalidEpoch(0x0301) - This is a protocol violation
8.6 Test Case D: Old Epoch After Overlap Expires
State: TRANSITION complete, epoch 1 confirmed, overlap window expired
Packet
EncryptedHeader: epoch_id: 00 00 00 00 ; epoch 0 (old, overlap expired) ...
Expected Behavior
- REJECT (epoch 0 no longer valid after overlap expires)
- Drop silently
- Only epoch 1+ packets accepted
8.7 Validation Checklist
An implementation passes if:
- Packets with unarmed epoch_id are rejected
- Epoch jumps (skipping epochs) are rejected
- Client unilateral epoch increment is rejected
- Old epoch packets after overlap expires are rejected
- Epoch advancement requires authenticated CTRL_REKEY
9. Nonce Reuse Detection
9.1 Purpose
This vector ensures implementations:
- Detect catastrophic nonce reuse as a receiver-safety diagnostic
- Terminate session immediately upon nonce reuse detection
- Log critical security violations
- Do NOT automatically reconnect
9.2 Normative Requirements
Per Section 9.6:
If a receiver observes a second packet with the same (epoch_id, seq) pair where the (ciphertext || tag) differs from the first observed instance, this indicates catastrophic nonce reuse. The receiver MUST immediately terminate the session, MUST log at CRITICAL level, and MUST NOT automatically reconnect.
Important
This detection occurs prior to AEAD decryption by comparing the incoming (ciphertext || tag) against a cached value from the previously accepted packet. The receiver does NOT attempt to authenticate the second packet.
Processing Order
Replay-window checks occur before AEAD; nonce-reuse detection refines replay handling by distinguishing "same packet" (drop) from "different packet, same nonce" (fatal).
9.3 Detection Mechanism
To minimize CPU usage and detect nonce reuse before AEAD:
- When a packet is accepted (AEAD verified, replay check passed):
- Cache:
(epoch_id, seq)→tag(or hash of ciphertext || tag) - Cache size: At least replay window size (e.g., 1024 entries)
- Cache:
- When a packet arrives with previously-seen
(epoch_id, seq):- Before AEAD: Compare incoming tag against cached tag
- If tags match: Normal replay, drop silently (no AEAD attempt)
- If tags differ: NONCE REUSE DETECTED → terminate immediately
9.4 Test Case: Same (epoch, seq) with Different Ciphertext/Tag
State: ESTABLISHED, epoch 0
Packet 1
(78 bytes, canonical format):
PublicHeader (30 bytes): 51 50 12 00 00 4E [24 bytes RID] Ciphertext: C1 = [32 bytes] Tag: T1 = A1 B2 C3 D4 E5 F6 07 18 29 3A 4B 5C 6D 7E 8F 90
Processing: AEAD verifies → ACCEPT → Cache (epoch=0, seq=1) → T1
Packet 2
(same epoch/seq, different ciphertext/tag):
PublicHeader (30 bytes): 51 50 12 00 00 4E [24 bytes RID] Ciphertext: C2 = [32 bytes, C2 ≠ C1] Tag: T2 = FF EE DD CC BB AA 99 88 77 66 55 44 33 22 11 00
Condition: T2 ≠ T1 (detected by tag comparison, no AEAD attempt)
9.5 Required Behavior
Upon detecting (epoch_id, seq) with different tag than cached:
- Do NOT attempt AEAD decryption (CPU protection)
- Immediately terminate session (fatal error)
- Log at CRITICAL level: "Nonce reuse detected: (epoch=X, seq=Y)"
- Zeroize all session keys immediately
- Do NOT automatically reconnect (requires user/admin intervention)
- MAY increment counter
ErrorInternalError(0xFF00)
Rationale
Nonce reuse under AEAD completely breaks confidentiality (XOR of plaintexts revealed) and may enable forgery attacks. This is catastrophic. By detecting via tag comparison before AEAD, we avoid CPU exhaustion from attackers sending many forged packets with reused (epoch, seq).
9.6 Distinguishing Replay from Nonce Reuse
| Condition | Detection | Action |
|---|---|---|
| Same (epoch, seq), same tag | Replay | Drop silently |
| Same (epoch, seq), different tag | Nonce reuse | Terminate session, CRITICAL log |
9.7 Validation Checklist
An implementation passes if:
- Maintains cache of
(epoch_id, seq)→tagfor accepted packets - Detects different tag for same (epoch, seq) before AEAD attempt
- Session is terminated immediately upon nonce reuse detection
- CRITICAL level log entry is generated
- No automatic reconnection occurs
- All session keys are zeroized
- Normal replays (same tag) are dropped silently without AEAD
10. Invalid Control Frames
10.1 Purpose
This vector ensures implementations:
- Reject unknown or reserved control frame types
- Validate control frame structure and length
- Reject malformed control payloads
10.2 Normative Requirements
Per Section 14:
- Unknown control types MUST be rejected
- CTRL_NONE (0x00) is reserved and MUST NOT be used
- Control frame length must match expected payload size
10.3 Test Case A: Reserved Control Type (CTRL_NONE)
Control Frame
ctrl_type: 00 ; CTRL_NONE (reserved, invalid) length: 00 00 00 ; length = 0 (3 bytes, big-endian u24) ctrl_data: <empty>
Expected Behavior
- REJECT (CTRL_NONE is reserved)
- Emit
ErrorInvalidControlFrame(0x0600) - Do NOT process as valid control
10.4 Test Case B: Unknown Control Type
Control Frame
ctrl_type: FF ; Unknown type length: 00 00 04 ; length = 4 (3 bytes, big-endian u24) ctrl_data: DE AD BE EF
Expected Behavior
- REJECT (unknown control type)
- Emit
ErrorUnknownControlType(0x0601) - Do NOT process payload
10.5 Test Case C: CTRL_REKEY with Invalid Length
Per Section 11.3, CTRL_REKEY has empty ctrl_data (length = 0). Epoch keys are derived locally using HKDF-Expand(epoch_n_secret, label("epoch step"), 32), not transmitted in the control frame.
Control Frame
ctrl_type: 01 ; CTRL_REKEY length: 00 00 10 ; length = 16 (INVALID: should be 0) ctrl_data: [16 bytes of garbage]
Expected Behavior
- REJECT (invalid length for CTRL_REKEY; expected 0)
- Emit
ErrorInvalidControlFrame(0x0600) - Do NOT process as rekey
10.6 Test Case D: CTRL_MIGRATE with Invalid Reason
Per Section 8.3, CTRL_MIGRATE has a 40-byte payload with the following structure:
migrate_nonce(32 bytes)peer_epoch_observed(4 bytes, big-endian u32)reason(1 byte, must be 0x01)reserved(3 bytes, must be 0x000000)
Control Frame
ctrl_type: 02 ; CTRL_MIGRATE length: 00 00 28 ; length = 40 (3 bytes, big-endian u24) ctrl_data: migrate_nonce: [32 bytes] peer_epoch_observed: 00 00 00 00 reason: 00 ; INVALID (must be 0x01) reserved: 00 00 00
Expected Behavior
- REJECT (invalid reason code)
- Emit
ErrorInvalidControlFrame(0x0600)
10.7 Test Case E: CTRL_MIGRATE with Non-Zero Reserved
Control Frame
ctrl_type: 02 ; CTRL_MIGRATE length: 00 00 28 ; length = 40 (3 bytes, big-endian u24) ctrl_data: migrate_nonce: [32 bytes] peer_epoch_observed: 00 00 00 00 reason: 01 ; valid reserved: FF 00 00 ; INVALID (must be 0x000000)
Expected Behavior
- REJECT (non-zero reserved field)
- Emit
ErrorInvalidControlFrame(0x0600)
10.8 Test Case F: ControlFrame Length Exceeds Available Plaintext
This tests bounds checking when ctrl_data length exceeds available plaintext.
Packet Construction
(78 bytes total):
After successful AEAD decryption, plaintext (32 bytes) contains:
EncryptedHeader (16 bytes): epoch_id: 00 00 00 00 seq: 00 00 00 00 00 00 00 01 padding_len: 00 00 stream_id: 00 hdr_flags: 00 ControlFrame (starting at byte 16): ctrl_type: 02 ; CTRL_MIGRATE length: 00 00 28 ; length = 40 (claims 40 bytes of ctrl_data) ctrl_data: [only 12 bytes available before end of plaintext]
Condition: Control frame claims 40 bytes of ctrl_data, but only 12 bytes remain in plaintext after the 4-byte control frame header (32 - 16 - 4 = 12).
Required Behavior
- AEAD decryption succeeds
- Parse EncryptedHeader (valid)
- Read control frame header:
ctrl_type = 0x02,length = 40 - Detect:
length (40) > available bytes (12)→ BOUNDS ERROR - Reject packet post-decryption
- Do NOT read beyond plaintext bounds
- Do NOT process partial control frame
- Drop silently
- MAY increment counter
ErrorInvalidControlFrame(0x0600)
10.9 Validation Checklist
An implementation passes if:
- CTRL_NONE (0x00) is rejected
- Unknown control types (0xFF, etc.) are rejected
- Control frames with incorrect length are rejected
- CTRL_MIGRATE with invalid reason is rejected
- CTRL_MIGRATE with non-zero reserved is rejected
- ControlFrame length exceeding plaintext causes rejection
- Valid control frames are processed correctly
Appendix A: Error Code Reference
| Error Condition | Error Code | Behavior |
|---|---|---|
| Invalid version | ErrorInvalidVersion (0x0100) | Reject, close |
| Signature verification failure | ErrorSignatureVerifyFailed (0x0106) | Abort, close |
| AEAD decryption failure | ErrorAEADDecryptFailed (0x0300) | Drop silently |
| Invalid epoch | ErrorInvalidEpoch (0x0301) | Drop or close |
| Replay detected | ErrorReplayDetected (0x0303) | Drop silently |
| Packet too large | ErrorPacketTooLarge (0x0305) | Drop silently |
| Early data rejected | ErrorEarlyDataRejected (0x0403) | Reject early data |
| Invalid control frame | ErrorInvalidControlFrame (0x0600) | Drop or close |
| Unknown control type | ErrorUnknownControlType (0x0601) | Drop |
| Nonce reuse detected | ErrorInternalError (0xFF00) | CRITICAL, terminate |
| Invalid magic | (silent drop) | Drop immediately |
| Reserved flags set | (silent drop) | Drop immediately |
| hdr_flags non-zero | (silent drop) | Drop |
Appendix B: Test Matrix
| Test ID | Category | Input | Expected Result |
|---|---|---|---|
| N1.1 | Handshake | Tampered CT_c | Signature verification failure (0x0106) |
| N1.2 | Handshake | Tampered server_nonce | Signature verification failure (0x0106) |
| N2.1 | Replay | Replay same (epoch, seq) | Second packet rejected |
| N2.2 | Replay | Out-of-order then replay | Reordered accepted, replay rejected |
| N2.3 | Replay | Sequence 0 first packet | Accepted (seq=0 valid for regular) |
| N3.1 | Size | Packet = 61 bytes | Rejected before decryption |
| N3.2 | Size | Packet = 1501 bytes | Rejected before decryption (0x0305) |
| N3.3 | Size | flags = 0x02 | Rejected (reserved bit) |
| N3.4 | Size | flags = 0xFA | Rejected (reserved bits) |
| N3.5 | Size | hdr_flags = 0x01 | Rejected after decryption |
| N3.6 | AEAD | Corrupted tag | AEAD fails, silent drop (0x0300) |
| N3.7 | Parser | padding_len overflow | Rejected post-decryption |
| N3.8 | stream_id | stream_id = 0xFF | Not delivered to app (STREAM_DUMMY) |
| N4.1 | Early Data | 1025 bytes (limit 1024) | Excess rejected (0x0403) |
| N4.2 | Early Data | Client claims 65535 | Ticket limit (1024) enforced |
| N4.3 | Early Data | Outside early-data phase | Rejected (0x0301) |
| N4.4 | Early Data | seq = 0 | Rejected (must be >= 1) |
| N5.1 | Magic | magic = 0x0000 | Rejected immediately |
| N5.2 | Magic | magic = 0x5051 | Rejected immediately |
| N6.1 | Version | version = 0x00 | Rejected (0x0100) |
| N6.2 | Version | version = 0xFF | Rejected (0x0100) |
| N7.1 | key_phase | key_phase=1 in STEADY | Rejected |
| N7.2 | key_phase | key_phase=1 on early data | Rejected |
| N7.3 | key_phase | key_phase=1 without armed epoch | Rejected |
| N8.1 | Epoch | Unarmed epoch_id | Rejected (0x0301) |
| N8.2 | Epoch | Epoch skip (0→2) | Rejected (0x0301) |
| N8.3 | Epoch | Client unilateral increment | Rejected (0x0301) |
| N9.1 | Nonce Reuse | Same (epoch,seq), different ciphertext | Session terminated, CRITICAL log |
| N10.1 | Control | ctrl_type = 0x00 | Rejected (0x0600) |
| N10.2 | Control | ctrl_type = 0xFF | Rejected (0x0601) |
| N10.3 | Control | CTRL_REKEY invalid length | Rejected (0x0600) |
| N10.4 | Control | CTRL_MIGRATE invalid reason | Rejected (0x0600) |
| N10.5 | Control | CTRL_MIGRATE non-zero reserved | Rejected (0x0600) |
| N10.6 | Control | ControlFrame length > plaintext | Rejected (0x0600) |
Appendix C: Relationship to Security Invariants
These negative vectors verify the following security invariants:
| Invariant # | Description | Tested By |
|---|---|---|
| 1 | Receiver sovereignty over epochs | N2, N4, N8 |
| 2 | Replayable data cannot steer state | N4 |
| 3 | Commit points are explicit | N4 |
| 4 | Fail-fast on ambiguity | N3 (reserved flags), N5 (magic), N6 (version) |
| 6 | (epoch_id, seq) uniqueness | N2, N9 |
| 7 | Replay window enforcement | N2 |
| 9 | key_phase validation | N7 |
| 10 | max_early_data_bytes from ticket | N4 |
| 11 | Early data is replayable | N4 |
| 12 | Epoch sovereignty | N8 |
| 18 | Global 128 KB size cap | N3 |
| 19 | Nonce uniqueness | N9 |
Appendix D: Error Code Coverage
| Error Code | Name | Tested? | Test ID |
|---|---|---|---|
| 0x0100 | ErrorInvalidVersion | ✅ | N6.1, N6.2 |
| 0x0106 | ErrorSignatureVerifyFailed | ✅ | N1.1, N1.2 |
| 0x0300 | ErrorAEADDecryptFailed | ✅ | N3.6 (explicit), N7, N8 (implicit) |
| 0x0301 | ErrorInvalidEpoch | ✅ | N4.3, N8.1, N8.2, N8.3 |
| 0x0303 | ErrorReplayDetected | ✅ | N2.1, N2.2 |
| 0x0305 | ErrorPacketTooLarge | ✅ | N3.2 |
| 0x0403 | ErrorEarlyDataRejected | ✅ | N4.1, N4.2 |
| 0x0600 | ErrorInvalidControlFrame | ✅ | N10.1, N10.3, N10.4, N10.5, N10.6 |
| 0x0601 | ErrorUnknownControlType | ✅ | N10.2 |
| 0xFF00 | ErrorInternalError | ✅ | N9.1 |
Coverage: 100% of critical error codes have explicit test cases.
PALISADE Protocol Specification Draft 00
INFORMATIONAL