golang/go · error
ecdh: private key does not support Bytes
Error message
ecdh: private key does not support Bytes
What it means
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.
Source
Thrown at src/crypto/hpke/kem.go:353
}
func (k *dhKEMPrivateKey) KEM() KEM {
return k.kem
}
func (k *dhKEMPrivateKey) Bytes() ([]byte, error) {
// Bizarrely, RFC 9180, Section 7.1.2 says SerializePrivateKey MUST clamp
// the output, which I thought we all agreed to instead do as part of the DH
// function, letting private keys be random bytes.
//
// At the same time, it says DeserializePrivateKey MUST also clamp, implying
// that the input doesn't have to be clamped, so Bytes by spec doesn't
// necessarily match the NewPrivateKey input.
//
// I'm sure this will not lead to any unexpected behavior or interop issue.
priv, ok := k.priv.(*ecdh.PrivateKey)
if !ok {
return nil, errors.New("ecdh: private key does not support Bytes")
}
if k.kem == dhKEMX25519 {
b := priv.Bytes()
b[0] &= 248
b[31] &= 127
b[31] |= 64
return b, nil
}
return priv.Bytes(), nil
}
func (k *dhKEMPrivateKey) PublicKey() PublicKey {
return &dhKEMPublicKey{
kem: k.kem,
pub: k.priv.PublicKey(),
}
}
View on GitHub (pinned to b6b368adc5)
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.
Example fix
// before sk, _ := hpke.NewDHKEMPrivateKey(hsmKeyExchanger) raw, err := sk.Bytes() // "ecdh: private key does not support Bytes" // after // persist the seed instead, and reconstruct via NewPrivateKey priv, _ := ecdh.X25519().NewPrivateKey(seed) sk, _ := hpke.NewDHKEMPrivateKey(priv) raw, err := sk.Bytes()
Defensive patterns
Strategy: type-guard
Validate before calling
// Only attempt Bytes() when the underlying key is the stdlib type.
func privateKeyBytes(k hpke.PrivateKey) ([]byte, error) {
// Round-trip only works for software keys; persist the seed otherwise.
b, err := k.Bytes()
if err != nil && err.Error() == "ecdh: private key does not support Bytes" {
return nil, fmt.Errorf("underlying KeyExchanger hides scalar; persist seed externally")
}
return b, err
} Type guard
func isStdlibECDH(k ecdh.KeyExchanger) bool {
_, ok := k.(*ecdh.PrivateKey)
return ok
} Try / catch
raw, err := sk.Bytes()
if err != nil {
if err.Error() == "ecdh: private key does not support Bytes" {
// fall back to a previously stored seed, not to fabricating bytes
raw = storedSeed
} else {
return err
}
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: HSM-backed key that only exposes ECDH() not Bytes(); test stubs implementing ecdh.KeyExchanger; proxy KeyExchanger that wraps a remote key service.
Related errors
- unsupported curve
- unsupported KEM
- unsupported curve
- private key seed not available
- crypto/ecdh: private key and public key curves do not match
AI-assisted analysis of golang/go@b6b368adc5 (2026-08-12).
Data as JSON: /api/errors/39431440817ae22e.
Report an issue: GitHub.