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
- 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.
- 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.
- Check that all server instances behind the load balancer share the same keypair from the opennhp/demo secret.
- 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
- Rotate keys via generate-nhp-keys.sh WITHOUT --regenerate, or redeploy all peers in lockstep when using --regenerate.
- Ensure every server instance behind a LB uses the same keypair.
- Validate UDP payload lengths before parsing to catch truncation/MTU issues.
- Monitor failure spikes per source IP and firewall obvious scanners.
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
- extractInitiatorStaticPubKey: aead
- invalid input key
- missing remote peer public key
- extractInitiatorStaticPubKey: init hash
- failed to create chain hash
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)