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

  1. Avoid calling Bytes() on hardware-backed DHKEM private keys; persist the original seed/ikm instead.
  2. Use NewPrivateKey(ikm) rather than NewDHKEMPrivateKey(customKeyExchanger) when serialization is required.
  3. 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

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


AI-assisted analysis of golang/go@b6b368adc5 (2026-08-12). Data as JSON: /api/errors/39431440817ae22e. Report an issue: GitHub.