{"record":{"id":"8336be5c3dfa16f7","repo":"OpenNHP/opennhp","slug":"extractinitiatorstaticpubkey-open-w","errorCode":null,"errorMessage":"extractInitiatorStaticPubKey: open: %w","messagePattern":"extractInitiatorStaticPubKey: open: %w","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"nhp/core/responder.go","lineNumber":187,"sourceCode":"\t\treturn nil, fmt.Errorf(\"extractInitiatorStaticPubKey: aead: %w\", err)\n\t}\n\t// Trust the AEAD's returned plaintext length over a static\n\t// scheme→size mapping. The previous version allocated a\n\t// PublicKeySizeEx-sized buffer and sliced it back down based on\n\t// header.CipherScheme(); that worked because the only two ciphers\n\t// today happen to match the scheme→size table exactly, but a\n\t// future cipher whose plaintext length doesn't fit either fixed\n\t// size would silently mis-key the cookie HMAC (Open writes\n\t// however many bytes the AEAD decrypted, then the caller would\n\t// either truncate them or hash trailing zero-padding).\n\t//\n\t// Validate the length explicitly before returning so future\n\t// breakage manifests as an error here, not as cookie failures\n\t// further down. Pass nil for the dst so Open allocates exactly\n\t// the right size.\n\tpeerPk, err := aead.Open(nil, header.NonceBytes(), header.StaticBytes(), chainHash.Sum(nil))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"extractInitiatorStaticPubKey: open: %w\", err)\n\t}\n\tswitch header.CipherScheme() {\n\tcase common.CIPHER_SCHEME_CURVE:\n\t\tif len(peerPk) != PublicKeySize {\n\t\t\treturn nil, fmt.Errorf(\"extractInitiatorStaticPubKey: curve scheme expected %d-byte pubkey, got %d\", PublicKeySize, len(peerPk))\n\t\t}\n\tcase common.CIPHER_SCHEME_GMSM:\n\t\tif len(peerPk) != PublicKeySizeEx {\n\t\t\treturn nil, fmt.Errorf(\"extractInitiatorStaticPubKey: gmsm scheme expected %d-byte pubkey, got %d\", PublicKeySizeEx, len(peerPk))\n\t\t}\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"extractInitiatorStaticPubKey: unknown cipher scheme %d (pubkey length %d)\", header.CipherScheme(), len(peerPk))\n\t}\n\treturn peerPk, nil\n}\n\ntype ResponderScheme interface {\n\tCreatePacketParserData(d *Device, pd *PacketData) (ppd *PacketParserData, err error)","sourceCodeStart":169,"sourceCodeEnd":205,"githubUrl":"https://github.com/OpenNHP/opennhp/blob/6e04ca5ff03222a699c24205cd4bf8fee9af7ffe/nhp/core/responder.go#L169-L205","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before (agent)\nserverPubKey = \"stale-key-after-rotation\"\n// after\nserverPubKey = <current value of nhp_server_public_key from opennhp/demo>","handlingStrategy":"try-catch","validationCode":"// verify packet header parses and has minimum static/nonce length before deep processing\nif len(pkt) < minHeaderLen+nonceSize+staticSize(cipherScheme) {\n\treturn fmt.Errorf(\"packet too short for scheme %d\", cipherScheme)\n}","typeGuard":null,"tryCatchPattern":"peerPk, err := aead.Open(nil, header.NonceBytes(), header.StaticBytes(), chainHash.Sum(nil))\nif err != nil {\n\t// expected for garbage/scans/rotated keys — rate-limit, log sparsely, drop\n\tmetrics.Count(\"static_open_failure\")\n\treturn nil, fmt.Errorf(\"extractInitiatorStaticPubKey: open: %w\", err)\n}","preventionTips":["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."],"tags":["go","cryptography","aead","gcm-authentication","packet-decryption"],"backgroundTag":"checksum-mismatch","analyzedSha":"6e04ca5ff03222a699c24205cd4bf8fee9af7ffe","analyzedAt":"2026-09-07T15:44:59.941Z","contentChangedAt":"2026-09-07T15:44:59.941Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}