{"record":{"id":"2f9b6dbf0582eb68","repo":"OpenNHP/opennhp","slug":"extractinitiatorstaticpubkey-curve-scheme-expecte","errorCode":null,"errorMessage":"extractInitiatorStaticPubKey: curve scheme expected %d-byte pubkey, got %d","messagePattern":"extractInitiatorStaticPubKey: curve scheme expected (.+?)-byte pubkey, got (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"nhp/core/responder.go","lineNumber":192,"sourceCode":"\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)\n\tDerivePacketParserDataFromPrevAssemblerData(mad *MsgAssemblerData, pkt *Packet, initTime int64) (ppd *PacketParserData)\n\tvalidatePeer(d *Device, ppd *PacketParserData) (err error)\n\tdecryptBody(d *Device, ppd *PacketParserData) (err error)\n}\n","sourceCodeStart":174,"sourceCodeEnd":210,"githubUrl":"https://github.com/OpenNHP/opennhp/blob/6e04ca5ff03222a699c24205cd4bf8fee9af7ffe/nhp/core/responder.go#L174-L210","documentation":"After successfully AEAD-opening the static field, extractInitiatorStaticPubKey validates that the plaintext length matches the public key size declared by the header's cipher scheme. For CIPHER_SCHEME_CURVE it must be exactly PublicKeySize (32 bytes). A mismatch means the decrypted plaintext is not a Curve25519 public key — the scheme field in the header does not match the actual key material the initiator encrypted.","triggerScenarios":"A sender encrypts a GMSM/SM2 public key but stamps the header with CIPHER_SCHEME_CURVE (sender/receiver scheme tables out of sync); a custom or future cipher writes a different plaintext length; header CipherScheme bits corrupted while the GCM tag still passed (unlikely) — realistically a scheme mismatch between agent build and server build.","commonSituations":"Mixed-version deployments where the agent and server disagree on the scheme→keysize mapping; custom forks that added a third cipher scheme and reused the CURVE constant; hand-crafted packets from tooling that builds headers manually.","solutions":["Ensure the agent sets the header's CipherScheme to the same constant it used for key generation/ECDH (CURVE keys → CIPHER_SCHEME_CURVE).","Log the received length and scheme at the sender side before sending to confirm they agree.","Align agent and server on the same opennhp version so PublicKeySize and the scheme table match.","If a new cipher was added, extend the switch in extractInitiatorStaticPubKey to its own case instead of reusing CURVE."],"exampleFix":"// before (sender)\nhdr.SetCipherScheme(common.CIPHER_SCHEME_CURVE) // but key is SM2 65-byte\n// after\nhdr.SetCipherScheme(common.CIPHER_SCHEME_GMSM) // matches key size","handlingStrategy":"validation","validationCode":"if header.CipherScheme() == common.CIPHER_SCHEME_CURVE && senderKeyType != keyTypeCurve {\n\treturn fmt.Errorf(\"sender key type does not match CURVE scheme\")\n}","typeGuard":"func schemeMatchesKey(scheme int, pk []byte) bool {\n\tif scheme == common.CIPHER_SCHEME_CURVE {\n\t\treturn len(pk) == core.PublicKeySize\n\t}\n\treturn len(pk) == core.PublicKeySizeEx\n}","tryCatchPattern":"peerPk, err := extractInitiatorStaticPubKey(dev, ciphers, header)\nif err != nil {\n\tif strings.Contains(err.Error(), \"expected \"+strconv.Itoa(core.PublicKeySize)+\"-byte\") {\n\t\tlog.Error(\"scheme/keysize mismatch — check agent build vs server build\")\n\t}\n\treturn err\n}","preventionTips":["Keep agent and server on the same release so scheme→keysize tables match.","Stamp header CipherScheme from the same constant used for key generation.","Add a sender-side assertion that the key length matches the configured scheme before sending.","Never reuse a scheme constant for a custom cipher with different key sizes."],"tags":["go","protocol","key-size","scheme-mismatch"],"backgroundTag":"unexpected-response-shape","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"}