slackhq/nebula · error

key was not %d bytes, is invalid %s public key

Error message

key was not %d bytes, is invalid %s public key

What it means

The PEM banner was valid but the block body length does not match the expected raw key size: 32 bytes for X25519 (CURVE25519) or 65 bytes for an uncompressed P256 public key. The parsed Curve name is interpolated into the message so you can see which key type was expected.

Source

Thrown at cert/pem.go:173

	k, r := pem.Decode(b)
	if k == nil {
		return nil, r, 0, fmt.Errorf("input did not contain a valid PEM encoded block")
	}
	var expectedLen int
	var curve Curve
	switch k.Type {
	case X25519PublicKeyBanner:
		expectedLen = 32
		curve = Curve_CURVE25519
	case P256PublicKeyBanner:
		// Uncompressed
		expectedLen = 65
		curve = Curve_P256
	default:
		return nil, r, 0, fmt.Errorf("bytes did not contain a proper public key banner")
	}
	if len(k.Bytes) != expectedLen {
		return nil, r, 0, fmt.Errorf("key was not %d bytes, is invalid %s public key", expectedLen, curve)
	}
	return k.Bytes, r, curve, nil
}

// UnmarshalSigningPublicKeyFromPEM will try to unmarshal the first pem block in a byte array, returning any non
// consumed data or an error on failure. Only Ed25519/ECDSA public key banners are accepted.
// Use UnmarshalPublicKeyFromPEM for X25519/P256 (ECDH) banners.
func UnmarshalSigningPublicKeyFromPEM(b []byte) ([]byte, []byte, Curve, error) {
	k, r := pem.Decode(b)
	if k == nil {
		return nil, r, 0, fmt.Errorf("input did not contain a valid PEM encoded block")
	}
	var expectedLen int
	var curve Curve
	switch k.Type {
	case Ed25519PublicKeyBanner:
		expectedLen = 32
		curve = Curve_CURVE25519

View on GitHub (pinned to dd8f660c0a)

Solutions

  1. For P256 keys, ensure the point is in uncompressed form (0x04 prefix + 32-byte X + 32-byte Y = 65 bytes); re-export with uncompressed encoding
  2. For X25519 keys, ensure exactly 32 raw key bytes are in the PEM body
  3. Re-emit the public key via the library's Marshal path (e.g. certificate PublicKey marshalling) rather than hand-building the PEM

Example fix

// before
blk.Bytes = elliptic.MarshalCompressed(curve, x, y) // 33 bytes for P256
// after
blk.Bytes = elliptic.Marshal(curve, x, y) // 65 bytes, uncompressed
Defensive patterns

Strategy: validation

Validate before calling

func checkECDHKeyLen(b []byte) error {
	blk, _ := pem.Decode(b)
	if blk == nil {
		return fmt.Errorf("no PEM")
	}
	switch blk.Type {
	case cert.X25519PublicKeyBanner:
		if len(blk.Bytes) != 32 { return fmt.Errorf("X25519 key must be 32 bytes, got %d", len(blk.Bytes)) }
	case cert.P256PublicKeyBanner:
		if len(blk.Bytes) != 65 || blk.Bytes[0] != 0x04 { return fmt.Errorf("P256 key must be 65-byte uncompressed point") }
	}
	return nil
}

Try / catch

pub, rest, curve, err := cert.UnmarshalPublicKeyFromPEM(b)
if err != nil {
	if strings.Contains(err.Error(), "invalid") && strings.Contains(err.Error(), "public key") {
		return fmt.Errorf("key body length does not match banner; re-export the key in the expected encoding: %w", err)
	}
	return err
}

Prevention

When it happens

Trigger: Calling UnmarshalPublicKeyFromPEM with a correctly-bannered block whose k.Bytes is the wrong length — e.g. compressed (33-byte) P256 point, truncated body, extra padding inside the block, or a point-encoded P256 key from another tool.

Common situations: Exporting P256 public keys from OpenSSL in compressed form (33 bytes) instead of uncompressed (65 bytes), copy/paste truncating base64, or hand-assembling PEM blocks with wrong DER/bytes content.

Related errors


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