slackhq/nebula · error

key was not %d bytes, is invalid Ed25519 private key

Error message

key was not %d bytes, is invalid Ed25519 private key

What it means

The PEM block was recognized as an Ed25519 signing private key (Ed25519PrivateKeyBanner), but the decoded bytes are not ed25519.PrivateKeySize (64) bytes long. Ed25519 private keys are 64 bytes (seed + public half); anything else means the key body is damaged or was encoded incorrectly.

Source

Thrown at cert/pem.go:266

	}
	return k.Bytes, r, curve, nil
}

func UnmarshalSigningPrivateKeyFromPEM(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 curve Curve
	switch k.Type {
	case EncryptedEd25519PrivateKeyBanner:
		return nil, nil, Curve_CURVE25519, ErrPrivateKeyEncrypted
	case EncryptedECDSAP256PrivateKeyBanner:
		return nil, nil, Curve_P256, ErrPrivateKeyEncrypted
	case Ed25519PrivateKeyBanner:
		curve = Curve_CURVE25519
		if len(k.Bytes) != ed25519.PrivateKeySize {
			return nil, r, 0, fmt.Errorf("key was not %d bytes, is invalid Ed25519 private key", ed25519.PrivateKeySize)
		}
	case ECDSAP256PrivateKeyBanner:
		curve = Curve_P256
		if len(k.Bytes) != 32 {
			return nil, r, 0, fmt.Errorf("key was not 32 bytes, is invalid ECDSA P256 private key")
		}
	default:
		return nil, r, 0, fmt.Errorf("bytes did not contain a proper Ed25519/ECDSA private key banner")
	}
	return k.Bytes, r, curve, nil
}

View on GitHub (pinned to dd8f660c0a)

Solutions

  1. Regenerate the CA signing key with nebula-cert ca so a full 64-byte Ed25519 private key is emitted
  2. Decode the PEM body and verify it is exactly ed25519.PrivateKeySize (64) bytes before calling
  3. If you only have the 32-byte seed, expand it with ed25519.NewKeyFromSeed before PEM-encoding

Example fix

// before
pem.Encode(file, &pem.Block{Type: banner, Bytes: seed}) // 32 bytes
// after
full := ed25519.NewKeyFromSeed(seed)
pem.Encode(file, &pem.Block{Type: banner, Bytes: full}) // 64 bytes
Defensive patterns

Strategy: validation

Validate before calling

blk, _ := pem.Decode(data)
if blk != nil && blk.Type == "NEBULA ED25519 SIGNING PRIVATE KEY" && len(blk.Bytes) != ed25519.PrivateKeySize {
    return fmt.Errorf("Ed25519 signing key must be 64 bytes, got %d", len(blk.Bytes))
}

Type guard

func isValidEd25519SigningKey(b []byte) bool {
    blk, _ := pem.Decode(b)
    return blk != nil && blk.Type == "NEBULA ED25519 SIGNING PRIVATE KEY" && len(blk.Bytes) == ed25519.PrivateKeySize
}

Try / catch

key, _, _, err := nebula.UnmarshalSigningPrivateKeyFromPEM(raw)
if err != nil {
    return fmt.Errorf("Ed25519 signing key is malformed (expected 64 bytes): %w", err)
}

Prevention

When it happens

Trigger: Call UnmarshalSigningPrivateKeyFromPEM with a block of type Ed25519PrivateKeyBanner whose k.Bytes length != 64, e.g. a 32-byte seed pasted under the full-key banner or a truncated file.

Common situations: Storing only the 32-byte seed but labeling it as the full private key, truncation during copy/paste or through line-wrapping tools, or regenerating the CA and mixing old banner with new (short) material.

Related errors


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