8. Encrypted Packet Format

8.1 PublicHeader Wire Format (ABNF)

Using Augmented Backus-Naur Form (ABNF) as defined in RFC 5234:

8.1.1 PublicHeader

PublicHeader = magic version flags length rid

magic    = 2OCTET                ; 0x5150 ("QP")
version  = OCTET                 ; 0x12 for PALISADE
flags    = OCTET                 ; Bit field
length   = 2OCTET                ; Big-endian u16
rid      = 24OCTET               ; Routing Identifier (192 bits, structured)

; Flags field bit layout:
; Bit 0: control_frame (0 = data frame, 1 = control frame)
; Bit 1: RESERVED
; Bit 2: key_phase - selects current (0) or next (1) epoch keys
; Bits 3-7: RESERVED

Total PublicHeader Size: 30 bytes (fixed)

Reserved Flag Bits: For version 0x12, receivers MUST drop packets where reserved flag bits (1, 3-7) are non-zero. This requirement MAY be relaxed in future versions after ossification risks are evaluated.

Implementation Guidance: Check reserved bits using the bitmask:

if (flags & 0xFA) != 0 → drop packet silently

; Bitmask breakdown:
; Bit 1: 0x02
; Bits 3-7: 0xF8
; Combined: 0xFA

Rationale: Unknown flags MUST NOT trigger expensive processing paths. Strict early enforcement prevents protocol ossification and ensures attackers cannot trigger expensive behavior with crafted packets.

Example Wire Format (hexadecimal):

51 50                    ; magic (0x5150 = "QP")
12                       ; version (0x12)
04                       ; flags (0x04 = data frame, key_phase=0)
04 D2                    ; length (0x04D2 = 1234 bytes)
[24 bytes of RID]        ; Routing Identifier

8.1.2 RID Structure

The RID (Routing Identifier) is a 24-byte structured field that replaces the 5-tuple as the session identifier. It enables stable session identification independent of source IP:port, required for NAT rebinding tolerance and multi-server deployments.

RID (24 bytes) =
    steering_prefix  [0:8]    ; 8 bytes - stable across salt rotations
    rotation_epoch   [8:10]   ; 2 bytes - salt rotation period identifier
    privacy_suffix   [10:24]  ; 14 bytes - rotates with salt, provides unlinkability

; Derivation:
steering_prefix = HKDF(client_secret, "palisade rid steering", 8)
rotation_epoch  = current_salt_epoch (big-endian u16, wraps at 65535)
privacy_suffix  = HKDF(client_secret, salt || "palisade rid privacy", 14)

Component Purposes:

  • steering_prefix: Load balancers consistent-hash on this field; stable across salt rotations to maintain routing consistency.
  • rotation_epoch: Optimization hint for backend table lookup during grace periods.
  • privacy_suffix: Provides unlinkability across rotation periods (112 bits, birthday bound 2^56).

Security Note: RID is a routing/key-selection hint only; it does not authenticate anything. Authentication is provided by successful AEAD decryption.

8.1.3 RID as Session Identifier

RID is the session identifier; source address/port are transport attributes that MAY change without requiring re-authentication.

Receivers MUST use RID for session lookup, not source address. This enables sessions to survive NAT rebinding and roaming without re-authentication, representing a fundamental architectural change from 5-tuple-based session identification.

NAT Rebinding Behavior: Source address changes for a known RID (NAT rebinding) SHOULD be accepted without re-handshake, subject to rate limiting (see Security Considerations). This allows sessions to continue seamlessly when the client's network path changes.

Unknown RID Handling (Normative): If session lookup by RID fails, receivers MUST proceed directly to handshake or session resumption flow. Receivers MUST NOT attempt speculative decryption for unknown RIDs.

Security Rationale: This requirement prevents CPU exhaustion attacks using forged RIDs. An unknown RID indicates either a new client or a corrupted/forged packet; in either case, decryption would fail and waste resources. Attempting decryption with unknown keys is computationally expensive and provides no security benefit.

Implementation Note: Receivers SHOULD track and rate-limit handshake attempts from source addresses sending unknown RIDs to mitigate handshake flooding attacks. This provides defense-in-depth against denial-of-service attacks while maintaining the fail-fast behavior required for unknown RIDs.

8.1.4 Load Balancer Steering

Load balancers SHOULD use consistent hashing on the steering_prefix (RID bytes 0-7) for routing decisions.

The steering_prefix is stable across salt rotations, ensuring packets for the same session route to the same backend even when RID privacy components change. This design enables stable load balancer routing while maintaining privacy through salt rotation.

Warning: Load balancers MUST NOT use the full RID for hashing, as this would cause route changes on salt rotation, breaking session continuity and potentially causing packet reordering or connection drops.

8.1.5 Sharded Session Storage (Normative)

Explicit Hash Function Requirement: Implementations using sharded session storage MUST use an explicit, stable hash function (e.g., xxhash64, siphash-2-4, FNV-1a) for RID-based shard selection. Implementations MUST NOT rely on language-default map hashing, as this may vary across versions or restarts and cause session lookup failures.

The hash function used for sharding must produce identical results across all instances in a deployment and across process restarts. This ensures that all servers in a multi-server deployment route the same RID to the same shard, preventing session lookup failures and ensuring consistent routing behavior.

Recommended Hash Functions: xxhash64, siphash-2-4, or FNV-1a. These are fast, well-distributed, and have stable implementations across languages. They provide deterministic output that does not depend on process state, randomization seeds, or language version.

Warning: Using Go's default map hashing, Python's hash(), or similar language-specific hash functions is prohibited for sharding, as these may use randomized seeds or change between versions. Such behavior causes session lookup failures in production deployments where different server instances must route the same RID to the same shard.

Implementation Guidance: When implementing sharded session storage, explicitly import and use a stable hash function library. Verify that the hash function produces identical output for the same input across different processes, languages, and versions. Test shard selection consistency across server restarts and deployments.

The length field specifies the total packet length in bytes, including the PublicHeader itself.

Parsing Rules:

  1. Read 30 bytes for PublicHeader
  2. Verify magic == 0x5150, else discard packet
  3. Verify version == 0x12, else send version mismatch error
  4. Parse flags, check reserved bits (1, 3-7) are 0, else drop packet
  5. Parse length, validate <= MTU
  6. Extract rid for routing/key selection
  7. Read length - 30 bytes for encrypted payload

Implementation Note: Parsers MUST reject packets with unknown flag bits set (fail-closed behavior). This is a MUST requirement for reserved bits 1 and 3-7.


8.2 EncryptedHeader Wire Format (ABNF)

EncryptedHeader = epoch_id seq padding_len stream_id hdr_flags

epoch_id    = 4OCTET            ; Big-endian u32
seq         = 8OCTET            ; Big-endian u64
padding_len = 2OCTET            ; Big-endian u16
stream_id   = OCTET             ; u8
hdr_flags   = OCTET             ; u8 (reserved in current PALISADE version)

Total EncryptedHeader Size: 16 bytes (fixed)

stream_id values 0x00–0xFE are application-defined. 
0xFF is RESERVED as STREAM_DUMMY and MUST NOT be used for application data.

hdr_flags semantics (current version):
- All bits are RESERVED.
- Senders MUST set hdr_flags = 0x00.
- Receivers MUST reject packets with hdr_flags != 0x00.
Rationale: Control semantics (e.g., rekey, migration) are expressed only via encrypted control frames.
Reserving hdr_flags avoids redundant signaling, prevents state divergence, and preserves a 
fixed-size header for future versions.

Total EncryptedHeader Size: 16 bytes (fixed)

Note: EncryptedHeader is encrypted as part of AEAD plaintext.

Example Wire Format (hexadecimal, before encryption):

00 00 30 39              ; epoch_id (12345)
00 00 00 02 4C B0 16 2A  ; seq (9876543210)
00 80                    ; padding_len (128)
00                       ; stream_id (0)
00                       ; hdr_flags (0x00, reserved in current PALISADE version)

8.3 Control Frame Wire Format (ABNF)

ControlFrame = ctrl_type length ctrl_data

ctrl_type = OCTET               ; Control type code
length    = 3OCTET              ; Big-endian u24 (length of ctrl_data)
ctrl_data = *OCTET              ; Variable length data

; Control type codes:
; 0x00 = CTRL_NONE (reserved)
; 0x01 = CTRL_REKEY
; 0x02 = CTRL_MIGRATE
; 0x03 = CTRL_CLOSE
; 0x04-0xFF = Reserved for future use

CTRL_MIGRATE (Control Frame Payload)

When ControlFrame.ctrl_type = CTRL_MIGRATE, the ctrl_data field MUST be encoded as:

MigrateFrame =
    migrate_nonce         (32 bytes)
    peer_epoch_observed  (4 bytes, uint32, big-endian)
    reason               (1 byte)
    reserved             (3 bytes)

migrate_nonce        = 32OCTET        ; Fresh nonce for replay protection
peer_epoch_observed  = 4OCTET         ; Big-endian u32
reason               = OCTET          ; Migration reason code (normalized to 0x01)
reserved             = 3OCTET         ; MUST be 0x000000

; Reason codes (anti-fingerprinting):
; 0x01 = NORMALIZED (all migrations use this value)
; 0x02-0x0F = Reserved (MUST NOT be used)
; 0x10-0xFF = Reserved (MUST NOT be used)

Note: Field renamed from new_epoch_hint to peer_epoch_observed to clarify advisory nature. See Section 13 for usage constraints.

Field requirements:

migrate_nonce

  • MUST be generated using a cryptographically secure random number generator and is used for nonce-based migration replay protection.

peer_epoch_observed

  • Receipt of peer_epoch_observed MUST NOT by itself cause an epoch transition, key derivation, or replay-window advancement. This field is diagnostic context only.
  • Permitted uses: Logging and metrics, challenge generation decisions, debugging information
  • Prohibited uses: Epoch advancement, key precomputation based on hinted epoch, replay window modification

reason

  • MUST be set to 0x01. All other values are reserved and MUST be rejected.

reserved

  • MUST be set to zero on transmission and ignored on receipt.

Note: The ctrl_type value is carried by the outer ControlFrame and MUST NOT be duplicated inside ctrl_data.

Total MigrateFrame Size: 40 bytes (fixed)


8.4 Payload

ciphertext = AEAD_Encrypt(
    key   = c2s_key or s2c_key (based on direction),
    nonce = derived nonce (see Section 8),
    aad   = PublicHeader (30 bytes),
    pt    = EncryptedHeader || payload_data || padding
)

Total Wire Format:

[PublicHeader (30 bytes cleartext)] [AEAD ciphertext (variable)] [AEAD tag (16 bytes)]

Only the PublicHeader fields are visible to the network. All other metadata (epoch, sequence, padding length, stream ID, header flags) is encrypted.

Total Packet Overhead: 62 bytes (30 PublicHeader + 16 EncryptedHeader + 16 AEAD tag)

For the default MTU of 1500 bytes, maximum payload size is 1438 bytes (1500 - 62).


8.5 Packet Size Validation

PALISADE endpoints MUST validate packet sizes prior to decryption or further processing in order to prevent buffer overflows, parsing vulnerabilities, and denial-of-service attacks.

8.5.1 Packet Size Constraints (Normative)

Let:

  • PH = 30 bytes (PublicHeader)
  • EH = 16 bytes (EncryptedHeader plaintext)
  • TAG = 16 bytes (AEAD authentication tag)

The minimum valid PALISADE packet size is:

MIN_PACKET_SIZE = PH + EH + TAG = 62 bytes

Implementations MUST enforce the following constraints:

  • Packets with total length less than 62 bytes MUST be rejected.
  • Packets with total length greater than the configured MTU MUST be rejected.
  • The default configured MTU MUST NOT exceed 1500 bytes.
  • Packets exceeding the MTU MUST be silently dropped.
  • These checks MUST be performed before attempting AEAD decryption.

8.5.2 Payload Size Calculation

For a given MTU, the maximum allowable application payload is:

MAX_PAYLOAD = MTU − (PH + EH + TAG)

For the default MTU of 1500 bytes:

MAX_PAYLOAD = 1500 − 62 = 1438 bytes

Padding bytes are included within the encrypted payload and count toward this limit.


8.5.3 Validation Order (Mandatory)

Receivers MUST apply packet size validation in the following order:

  1. Verify total packet length ≥ MIN_PACKET_SIZE
  2. Verify total packet length ≤ configured MTU
  3. Verify that length fields do not cause buffer overrun
  4. Proceed to AEAD decryption only after all checks pass

If any validation step fails, the packet MUST be dropped immediately.


8.5.4 Error Handling Requirements

Packets rejected due to size violations:

  • MUST be dropped silently
  • MUST NOT generate error responses
  • MUST NOT trigger protocol state changes

This prevents amplification and reflection attacks.


8.5.5 Operational Guidance (Non-Normative)

Typical PALISADE packet sizes are significantly smaller than the permitted maximums:

  • Typical data packets: 200–1400 bytes
  • Minimum overhead: 40 bytes

The MTU limit exists to ensure robustness and future extensibility. Implementations MAY:

  • Log rejected packets for diagnostics (rate-limited)
  • Track metrics related to size violations
  • Support Path MTU Discovery (PMTUD) where available

Such behavior does not affect protocol correctness.

PALISADE Protocol Specification Draft 00

INFORMATIONAL