netbirdio/netbird · error

failed to parse private root key: %w

Error message

failed to parse private root key: %w

What it means

reposign.ParseRootKey failed in create-revocation-list: it expects a single PEM block of type "ROOT PRIVATE KEY" whose payload is JSON with a 64-byte Ed25519 private key (reposign/root.go:32-38, key.go:131-156). It rejects wrong PEM tags (passing the public root key or an artifact private key), trailing PEM data, undecodable PEM, bad JSON, and wrong key length.

Source

Thrown at client/cmd/signer/revocation.go:104

		panic(err)
	}
	if err := verifyRevocationListCmd.MarkFlagRequired("signature-file"); err != nil {
		panic(err)
	}
	if err := verifyRevocationListCmd.MarkFlagRequired("public-root-key"); err != nil {
		panic(err)
	}
}

func handleCreateRevocationList(cmd *cobra.Command, revocationListFile string, privateRootKeyFile string) error {
	privKeyPEM, err := os.ReadFile(privateRootKeyFile)
	if err != nil {
		return fmt.Errorf("failed to read private root key file: %w", err)
	}

	privateRootKey, err := reposign.ParseRootKey(privKeyPEM)
	if err != nil {
		return fmt.Errorf("failed to parse private root key: %w", err)
	}

	rlBytes, sigBytes, err := reposign.CreateRevocationList(*privateRootKey, expirationDuration)
	if err != nil {
		return fmt.Errorf("failed to create revocation list: %w", err)
	}

	if err := writeOutputFiles(revocationListFile, revocationListFile+".sig", rlBytes, sigBytes); err != nil {
		return fmt.Errorf("failed to write output files: %w", err)
	}

	cmd.Println("✅ Revocation list created successfully")
	return nil
}

func handleExtendRevocationList(cmd *cobra.Command, keyID, revocationListFile, privateRootKeyFile string) error {
	privKeyPEM, err := os.ReadFile(privateRootKeyFile)
	if err != nil {

View on GitHub (pinned to 93e97f4bf1)

Solutions

  1. Check head -1 of the file: it must read -----BEGIN ROOT PRIVATE KEY-----
  2. Confirm it is the private half of the root key pair from `signer generate-root-key`, not the public half and not the artifact key
  3. Ensure the PEM block structure survived copying (64-column base64 lines, single block)
  4. Regenerate the root key pair only as a last resort — it invalidates the whole existing chain of trust

Example fix

// before
./signer create-revocation-list --private-root-key root-key-pub.pem ...

// after
./signer create-revocation-list --private-root-key root-key-priv.pem ...
Defensive patterns

Strategy: type-guard

Type guard

func isRootPrivateKeyPEM(data []byte) bool {
    block, rest := pem.Decode(data)
    return block != nil && block.Type == "ROOT PRIVATE KEY" && len(rest) == 0
}

Prevention

When it happens

Trigger: Passing the ROOT PUBLIC KEY file (most common — same name, different suffix); passing the ARTIFACT PRIVATE KEY; PEM mangled when copied through a terminal or chat (lost newlines); two concatenated PEM blocks.

Common situations: Key vault stores root-key.pem (public) and root-key-priv.pem side by side and the wrong one is selected; key copied with newlines collapsed; key pasted with trailing content.

Understand the failure class

Related errors


AI-assisted analysis of netbirdio/netbird@93e97f4bf1 (2026-08-16). Data as JSON: /api/errors/12a0a0d76f3d519a. Report an issue: GitHub.