slackhq/nebula · error

unable to unmarshal private key: %w

Error message

unable to unmarshal private key: %w

What it means

This error is returned by nistCurve.DH when the local private key bytes cannot be parsed via ecdh.Curve.NewPrivateKey. It indicates the configured local private key is not a valid scalar for the NIST curve — wrong length, all zeros, or out of range. The underlying error is wrapped so the cause is preserved.

Source

Thrown at noiseutil/nist.go:51

	if rng == nil {
		rng = rand.Reader
	}
	privkey, err := c.curve.GenerateKey(rng)
	if err != nil {
		return noise.DHKey{}, err
	}
	pubkey := privkey.PublicKey()
	return noise.DHKey{Private: privkey.Bytes(), Public: pubkey.Bytes()}, nil
}

func (c nistCurve) DH(privkey, pubkey []byte) ([]byte, error) {
	ecdhPubKey, err := c.curve.NewPublicKey(pubkey)
	if err != nil {
		return nil, fmt.Errorf("unable to unmarshal pubkey: %w", err)
	}
	ecdhPrivKey, err := c.curve.NewPrivateKey(privkey)
	if err != nil {
		return nil, fmt.Errorf("unable to unmarshal private key: %w", err)
	}

	return ecdhPrivKey.ECDH(ecdhPubKey)
}

func (c nistCurve) DHLen() int {
	// NOTE: Noise Protocol specifies "DHLen" to represent two things:
	// - The size of the public key
	// - The return size of the DH() function
	// But for standard NIST ECDH, the sizes of these are different.
	// Luckily, the flynn/noise library actually only uses this DHLen()
	// value to represent the public key size, so that is what we are
	// returning here. The length of the DH() return bytes are unaffected by
	// this value here.
	return c.pubLen
}
func (c nistCurve) DHName() string { return c.name }

View on GitHub (pinned to dd8f660c0a)

Solutions

  1. Regenerate the local key pair with the same library/curve and update the config
  2. Ensure the private key is the raw scalar (32/48/66 bytes for P-256/P-384/P-521), not a PEM/DER/pkcs8 blob
  3. Check config parsing: confirm base64 decoding produced the full key (no whitespace truncation)
  4. Confirm the same curve is used for key generation and DH (e.g. P256 vs P384 mismatch)

Example fix

// before: passing a PEM block's raw bytes as the private key
priv := pemBlock.Bytes
shared, err := curve.DH(priv, pub)
// after: decode to a raw scalar of correct size first
priv, err := base64.StdEncoding.DecodeString(cfg.PrivateKey)
if len(priv) != 32 { return nil, fmt.Errorf("bad P-256 private key length %d", len(priv)) }
shared, err := curve.DH(priv, pub)
Defensive patterns

Strategy: validation

Validate before calling

func validNistPriv(curveName string, priv []byte) bool {
    var n int
    switch curveName {
    case "P256": n = 32
    case "P384": n = 48
    case "P521": n = 66
    default: return false
    }
    if len(priv) != n { return false }
    for _, b := range priv { if b != 0 { return true } }
    return false // all-zero scalar is invalid
}

Type guard

func isPrivUnmarshalError(err error) bool {
    return err != nil && strings.Contains(err.Error(), "unable to unmarshal private key")
}

Try / catch

shared, err := curve.DH(priv, pub)
if err != nil {
    if isPrivUnmarshalError(err) {
        return fmt.Errorf("local private key invalid for curve; regenerate keys: %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: Calling nistCurve.DH(privkey, pubkey) where privkey is empty, the wrong byte length for the curve, or numerically invalid as a private scalar for ecdh.Curve.NewPrivateKey.

Common situations: Hand-edited or base64-decoded config keys with missing/truncated bytes; a key generated for a different curve or algorithm; loading a PEM/DER private key file and passing the raw file bytes instead of the raw scalar.

Related errors


AI-assisted analysis of slackhq/nebula@dd8f660c0a (2026-09-03). Data as JSON: /api/errors/33c2b91963977f08. Report an issue: GitHub.