OpenNHP/opennhp · error

extractInitiatorStaticPubKey: open

Error message

extractInitiatorStaticPubKey: open: %w

What it means

The AEAD Open of the initiator's static public key from the packet header failed. The static field is encrypted with a key derived from the server's own ECDH private key and the packet's ephemeral key; Open fails when the ciphertext or its 16-byte GCM tag does not verify. This means the packet was not produced by an initiator that performed ECDH with this server's public key, or the header bytes were corrupted/truncated in transit.

Solutions

  1. Compare the agent's configured server public key with the server's config.toml private key; re-render deploy/configs and redeploy both sides after any key rotation.
  2. Capture the offending packet and verify header length/payload size — if shorter than the scheme expects, fix the sender or relay path rather than the crypto.
  3. Check that all server instances behind the load balancer share the same keypair from the opennhp/demo secret.
  4. If failures spike from one IP, treat it as scanning/probing; the cookie/overload path already rate-limits these, no fix needed on this side.

Example fix

// before (agent)
serverPubKey = "stale-key-after-rotation"
// after
serverPubKey = <current value of nhp_server_public_key from opennhp/demo>
Defensive patterns

Strategy: try-catch

Validate before calling

// verify packet header parses and has minimum static/nonce length before deep processing
if len(pkt) < minHeaderLen+nonceSize+staticSize(cipherScheme) {
	return fmt.Errorf("packet too short for scheme %d", cipherScheme)
}

Try / catch

peerPk, err := aead.Open(nil, header.NonceBytes(), header.StaticBytes(), chainHash.Sum(nil))
if err != nil {
	// expected for garbage/scans/rotated keys — rate-limit, log sparsely, drop
	metrics.Count("static_open_failure")
	return nil, fmt.Errorf("extractInitiatorStaticPubKey: open: %w", err)
}

Prevention

When it happens

Trigger: An agent sends a KNK/RKN encrypted against a different (stale or wrong) server public key; the UDP packet is truncated or bit-flipped; an attacker probes the port with forged headers; the relay forwards a mangled packet so header.StaticBytes()/NonceBytes() no longer align with the ciphertext.

Common situations: Server keys rotated via generate-nhp-keys.sh --regenerate while agents still run old config.toml peer tables; load balancer splitting handshake packets across server instances with mismatched keys; MTU/fragmentation corrupting UDP payloads; port scans hitting the open UDP socket.

Understand the failure class

Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.

Related errors


AI-assisted analysis of OpenNHP/opennhp@6e04ca5ff0 (2026-09-07). Data as JSON: /api/errors/8336be5c3dfa16f7. Report an issue: GitHub.

Appendix: source

Thrown at nhp/core/responder.go:187

		return nil, fmt.Errorf("extractInitiatorStaticPubKey: aead: %w", err)
	}
	// Trust the AEAD's returned plaintext length over a static
	// scheme→size mapping. The previous version allocated a
	// PublicKeySizeEx-sized buffer and sliced it back down based on
	// header.CipherScheme(); that worked because the only two ciphers
	// today happen to match the scheme→size table exactly, but a
	// future cipher whose plaintext length doesn't fit either fixed
	// size would silently mis-key the cookie HMAC (Open writes
	// however many bytes the AEAD decrypted, then the caller would
	// either truncate them or hash trailing zero-padding).
	//
	// Validate the length explicitly before returning so future
	// breakage manifests as an error here, not as cookie failures
	// further down. Pass nil for the dst so Open allocates exactly
	// the right size.
	peerPk, err := aead.Open(nil, header.NonceBytes(), header.StaticBytes(), chainHash.Sum(nil))
	if err != nil {
		return nil, fmt.Errorf("extractInitiatorStaticPubKey: open: %w", err)
	}
	switch header.CipherScheme() {
	case common.CIPHER_SCHEME_CURVE:
		if len(peerPk) != PublicKeySize {
			return nil, fmt.Errorf("extractInitiatorStaticPubKey: curve scheme expected %d-byte pubkey, got %d", PublicKeySize, len(peerPk))
		}
	case common.CIPHER_SCHEME_GMSM:
		if len(peerPk) != PublicKeySizeEx {
			return nil, fmt.Errorf("extractInitiatorStaticPubKey: gmsm scheme expected %d-byte pubkey, got %d", PublicKeySizeEx, len(peerPk))
		}
	default:
		return nil, fmt.Errorf("extractInitiatorStaticPubKey: unknown cipher scheme %d (pubkey length %d)", header.CipherScheme(), len(peerPk))
	}
	return peerPk, nil
}

type ResponderScheme interface {
	CreatePacketParserData(d *Device, pd *PacketData) (ppd *PacketParserData, err error)

View on GitHub (pinned to 6e04ca5ff0)