{"record":{"id":"39431440817ae22e","repo":"golang/go","slug":"ecdh-private-key-does-not-support-bytes","errorCode":null,"errorMessage":"ecdh: private key does not support Bytes","messagePattern":"ecdh: private key does not support Bytes","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/crypto/hpke/kem.go","lineNumber":353,"sourceCode":"}\n\nfunc (k *dhKEMPrivateKey) KEM() KEM {\n\treturn k.kem\n}\n\nfunc (k *dhKEMPrivateKey) Bytes() ([]byte, error) {\n\t// Bizarrely, RFC 9180, Section 7.1.2 says SerializePrivateKey MUST clamp\n\t// the output, which I thought we all agreed to instead do as part of the DH\n\t// function, letting private keys be random bytes.\n\t//\n\t// At the same time, it says DeserializePrivateKey MUST also clamp, implying\n\t// that the input doesn't have to be clamped, so Bytes by spec doesn't\n\t// necessarily match the NewPrivateKey input.\n\t//\n\t// I'm sure this will not lead to any unexpected behavior or interop issue.\n\tpriv, ok := k.priv.(*ecdh.PrivateKey)\n\tif !ok {\n\t\treturn nil, errors.New(\"ecdh: private key does not support Bytes\")\n\t}\n\tif k.kem == dhKEMX25519 {\n\t\tb := priv.Bytes()\n\t\tb[0] &= 248\n\t\tb[31] &= 127\n\t\tb[31] |= 64\n\t\treturn b, nil\n\t}\n\treturn priv.Bytes(), nil\n}\n\nfunc (k *dhKEMPrivateKey) PublicKey() PublicKey {\n\treturn &dhKEMPublicKey{\n\t\tkem: k.kem,\n\t\tpub: k.priv.PublicKey(),\n\t}\n}\n","sourceCodeStart":335,"sourceCodeEnd":371,"githubUrl":"https://github.com/golang/go/blob/b6b368adc57c96c3151d224d172029f233ead2c3/src/crypto/hpke/kem.go#L335-L371","documentation":"dhKEMPrivateKey.Bytes() implements RFC 9180 SerializePrivateKey, which requires raw access to the scalar (with clamping for X25519). When the underlying KeyExchanger is not the standard *ecdh.PrivateKey (e.g. an HSM-backed KeyExchanger that hides the scalar), the type assertion fails and Bytes refuses to fabricate a serialization. This is by design: SerializePrivateKey is impossible without the raw scalar.","triggerScenarios":"Calling Bytes() on a PrivateKey returned by NewDHKEMPrivateKey when the wrapped value is a custom ecdh.KeyExchanger implementation (not *ecdh.PrivateKey). Common with hardware keys or mock ECDH implementations used in testing.","commonSituations":"HSM-backed key that only exposes ECDH() not Bytes(); test stubs implementing ecdh.KeyExchanger; proxy KeyExchanger that wraps a remote key service.","solutions":["Avoid calling Bytes() on hardware-backed DHKEM private keys; persist the original seed/ikm instead.","Use NewPrivateKey(ikm) rather than NewDHKEMPrivateKey(customKeyExchanger) when serialization is required.","If the underlying value is really an *ecdh.PrivateKey but wrapped, unwrap it before calling NewDHKEMPrivateKey."],"exampleFix":"// before\nsk, _ := hpke.NewDHKEMPrivateKey(hsmKeyExchanger)\nraw, err := sk.Bytes() // \"ecdh: private key does not support Bytes\"\n\n// after\n// persist the seed instead, and reconstruct via NewPrivateKey\npriv, _ := ecdh.X25519().NewPrivateKey(seed)\nsk, _ := hpke.NewDHKEMPrivateKey(priv)\nraw, err := sk.Bytes()","handlingStrategy":"type-guard","validationCode":"// Only attempt Bytes() when the underlying key is the stdlib type.\nfunc privateKeyBytes(k hpke.PrivateKey) ([]byte, error) {\n    // Round-trip only works for software keys; persist the seed otherwise.\n    b, err := k.Bytes()\n    if err != nil && err.Error() == \"ecdh: private key does not support Bytes\" {\n        return nil, fmt.Errorf(\"underlying KeyExchanger hides scalar; persist seed externally\")\n    }\n    return b, err\n}","typeGuard":"func isStdlibECDH(k ecdh.KeyExchanger) bool {\n    _, ok := k.(*ecdh.PrivateKey)\n    return ok\n}","tryCatchPattern":"raw, err := sk.Bytes()\nif err != nil {\n    if err.Error() == \"ecdh: private key does not support Bytes\" {\n        // fall back to a previously stored seed, not to fabricating bytes\n        raw = storedSeed\n    } else {\n        return err\n    }\n}","preventionTips":["Persist the ikm/seed when you first create an HSM-backed key; do not rely on Bytes() later.","Reserve NewDHKEMPrivateKey for hardware keys that genuinely cannot expose their scalar.","For software keys, always go through dhKEM.NewPrivateKey to keep Bytes() valid."],"tags":["hpke","kem","ecdh","hardware","serialization","go"],"analyzedSha":"b6b368adc57c96c3151d224d172029f233ead2c3","analyzedAt":"2026-08-12T00:22:02.250Z","schemaVersion":2},"datasetVersion":"2026-08-12T08:17:17.861Z"}