Back to Opennhp

Message Header

docs/protocol/header.md

0.7.310.8 KB
Original Source

NHP Message Header

{: .fs-9 }

Every NHP packet begins with a fixed-length header that carries identity, ephemeral key material, replay-protection state, and an HMAC over the header itself. This page documents each field with its byte offset, the obfuscation scheme that hides public metadata in transit, and a pointer into the Go code that parses it. {: .fs-6 .fw-300 }

Implements CSA Stealth Mode SDP §NHP Message Header (Table 3). Constants defined in nhp/core/scheme/curve/header.go and nhp/core/scheme/gmsm/header.go; dispatch in nhp/core/packet.go.

{: .note } The CSA whitepaper quotes 160 / 224 bytes, matching an earlier draft in which the sender's static public key was the only encrypted identity material in the header. The current Go implementation also wraps an 80-byte IBC identity block, bringing the totals to 240 / 304 bytes. When the spec and the code disagree, this documentation tracks the code (per [Protocol Reference]({{ '/protocol/' | relative_url }}) §Spec version).

Layout

  • Standard (international cipher suite): 240 bytesCIPHER_SCHEME_CURVE (Curve25519, AES-256-GCM, BLAKE2s).
  • Extended (domestic / Chinese cipher suite): 304 bytesCIPHER_SCHEME_GMSM (SM2, SM4-GCM, SM3).

The AEAD-encrypted message payload (plaintext size + 16-byte GCM tag) follows the header. Total on-wire packet size is header + ciphertext, bounded by the UDP max of 65,535 bytes. TCP short-connection transport is also supported when UDP is unavailable.

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|         Leading Obfuscation (4, random, XOR mask source)      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|         Message Type (2) ‖ Message Length (2)  XOR-masked      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Ver Major (1) | Ver Minor (1) |       Protocol Flags (2)      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                       Reserved (4, zero)                      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                  Counter, big-endian (bytes 0–3)              |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                  Counter, big-endian (bytes 4–7)              |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
|        Ephemeral Public Key  (32 Curve25519 / 64 SM2)         |
|                            ...                                |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
|                IBC Identity Ciphertext (80, AEAD)             |
|                            ...                                |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
|         Static Public Key Ciphertext (48 / 80, AEAD)          |
|                            ...                                |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
|             Timestamp Ciphertext (24, AEAD-wrapped)           |
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
|                          HMAC (32)                            |
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
|              Encrypted message payload + tag (variable)       |
|                            ...                                |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Fields

Offsets and sizes are listed as Curve / GMSM pairs where they differ. Fields shift between the two schemes because the ephemeral public key is 32 bytes for Curve25519 versus 64 bytes (uncompressed X‖Y) for SM2, and the static public-key plaintext follows the same size split.

OffsetSize (bytes)FieldDescription
04Leading Obfuscation4 random bytes generated per packet. Used as a one-shot XOR mask for the 4-byte Type‖Length tuple that follows, so that passive observers cannot trivially identify the packet type or size. Plaintext on the wire; the receiver reads it first, then deobfuscates.
42Message TypeNHP message type ID (see [Message Types]({{ '/protocol/messages/'
62Message LengthLength of the ciphertext that follows the header, including the 16-byte AEAD tag, big-endian. XOR-masked together with Message Type. Max 65,535 (UDP limit).
81Protocol Major VersionPlaintext. Receivers silently discard packets with an unsupported major version (default-deny).
91Protocol Minor VersionPlaintext. Backward-compatible increments only.
102Protocol FlagsBig-endian bit flags (see table below). Unused bits in the lower 12 should be zero. Bit 12 is the cipher-scheme selector (0 = Curve, 1 = GMSM); bits 13–15 are reserved and sit alongside bit 12 in the top nibble so the selector can grow as new schemes land.
124ReservedZero in current senders; receivers ignore the contents. Forward-compatibility padding.
168Counter64-bit big-endian nonce and transaction tracker. The counter occupies bytes 4–11 of the 12-byte GCM nonce; bytes 0–3 are zero-padded. See NonceBytes. Monotonically increments per encryption; receivers reject stale counters for that session.
2432 or 64Ephemeral Public KeyFresh per-packet ephemeral public key — 32 bytes for Curve25519/X25519 (standard), 64 bytes for SM2 (extended, uncompressed X‖Y coordinates). Drives Noise handshake key derivation and provides forward secrecy even for single-shot message types.
56 / 8880IBC Identity CiphertextAEAD-encrypted IBC identity block: a fixed 64-byte plaintext slot (MaximumIdentitySize) — zero-padded when the identity is shorter, or entirely zero in PKI mode — followed by a 16-byte tag.
136 / 16848 or 80Static Public Key CiphertextAEAD-encrypted long-term static public key of the sender. Decrypted with keys derived from the ephemeral DH. Size = plaintext key (32 or 64 bytes) + 16-byte tag.
184 / 24824Timestamp CiphertextAEAD-encrypted 8-byte UNIX-milliseconds timestamp + 16-byte tag. Freshness check: receivers enforce a tolerance window to defeat replay while surviving clock skew.
208 / 27232HMACKeyed hash over every preceding header byte (offsets 0 through the byte immediately before this field) — i.e., the entire header excluding the HMAC itself; the payload ciphertext is not covered. For NHP-RKN, the previously issued cookie is appended to the hash input. Validated before AEAD decryption — a mismatched HMAC causes a silent drop, aligning with default-deny. See MsgAssemblerData.addHMAC.

Total header length: 240 bytes (standard) or 304 bytes (extended).

Protocol Flags (bit 0 = LSB)

BitNameMeaning
0NHP_FLAG_EXTENDEDLENGTHSet for the 304-byte GMSM header; clear for the 240-byte Curve header.
1NHP_FLAG_COMPRESSPayload ciphertext plaintext was zlib-compressed before encryption.
2NHP_FLAG_CL_PKCCL-PKC (certificate-less public-key cryptography) mode.
3–11Reserved.
12cipher-scheme selector0 = CIPHER_SCHEME_CURVE; 1 = CIPHER_SCHEME_GMSM.
13–15Reserved. Grouped with bit 12 as the top-nibble selector so additional schemes can be encoded without reflowing the field.

See nhp/common/packet.go for the authoritative constants.

Obfuscation scheme

The first eight bytes on the wire — Leading Obfuscation followed by the Type‖Length tuple — are the only pre-decryption surface. A single 4-byte XOR masks Type and Length together against the Leading Obfuscation word:

preamble      = wire[0..4]    // bytes 0–3, big-endian uint32 random
type_and_len  = wire[4..8]    // bytes 4–7, big-endian uint32, XOR-masked
decoded       = preamble XOR type_and_len
type          = (decoded >> 16) & 0xFFFF
length        = decoded        & 0xFFFF

Half-open ranges (a..b) mean "byte index a inclusive, byte index b exclusive", matching Go / Rust slice semantics.

This is defence-in-depth — the payload itself is AEAD-encrypted and the static key is AEAD-wrapped — but the obfuscation foils trivial traffic analysis and fingerprinting. See SetTypeAndPayloadSize / TypeAndPayloadSize in nhp/core/scheme/curve/header.go.

Version and flag handling

  • Version mismatch → silent discard. Never respond to mismatched protocol versions; that would reveal server presence.
  • Unknown flags → the lower 12 bits that are not defined in the table above should be zero. Receivers may silently discard or log-and-drop depending on deployment policy. Future PQC-hybrid modes are planned to land in Protocol Flags rather than forcing a major-version bump.

Concurrent sessions

Multiple simultaneous knocks from the same NHP-Agent (e.g., targeting different Protected Resources) are distinguished by:

  1. Fresh ephemeral keys per handshake — no collision even within the same second.
  2. Independent Noise state (CipherState + chaining key) per session.
  3. Session / transaction ID (payload-level, see the [NHP-ACK message]({{ '/protocol/messages/' | relative_url }})).

No sender-side coordination or locking is required.

See also

  • [Message Types]({{ '/protocol/messages/' | relative_url }}) — how each ID is handled after the header parses
  • [Cryptography]({{ '/cryptography/' | relative_url }}) — the algorithms the AEAD and Noise state rely on
  • [Glossary]({{ '/glossary/' | relative_url }}) — definitions for counter, ephemeral key, HMAC, cipher scheme