netbirdio/netbird · error

failed to parse signature: %w

Error message

failed to parse signature: %w

What it means

reposign.ParseSignature failed (signature.go:17): it is a plain json.Unmarshal into the Signature struct {signature []byte, timestamp time.Time, key_id KeyID, algorithm, hash_algo}. Failure means the .sig bytes are not valid JSON, have wrong field types, or carry a key_id string that the KeyID unmarshaler rejects (not 16 hex chars). Note json.Unmarshal ignores unknown and missing fields, so passing the revocation list itself as --signature-file parses successfully into zero values and blows up later in ValidateRevocationList with timestamp errors instead.

Source

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

		return fmt.Errorf("failed to read signature file: %w", err)
	}

	// Read public root key file
	pubKeyPEM, err := os.ReadFile(publicRootKeyFile)
	if err != nil {
		return fmt.Errorf("failed to read public root key file: %w", err)
	}

	// Parse public root key
	publicKey, err := reposign.ParseRootPublicKey(pubKeyPEM)
	if err != nil {
		return fmt.Errorf("failed to parse public root key: %w", err)
	}

	// Parse signature
	signature, err := reposign.ParseSignature(sigBytes)
	if err != nil {
		return fmt.Errorf("failed to parse signature: %w", err)
	}

	// Validate revocation list
	rl, err := reposign.ValidateRevocationList([]reposign.PublicKey{publicKey}, rlBytes, *signature)
	if err != nil {
		return fmt.Errorf("failed to validate revocation list: %w", err)
	}

	// Display results
	cmd.Println("✅ Revocation list signature is valid")
	cmd.Printf("Last Updated: %s\n", rl.LastUpdated.Format(time.RFC3339))
	cmd.Printf("Expires At: %s\n", rl.ExpiresAt.Format(time.RFC3339))
	cmd.Printf("Number of revoked keys: %d\n", len(rl.Revoked))

	if len(rl.Revoked) > 0 {
		cmd.Println("\nRevoked Keys:")
		for keyID, revokedTime := range rl.Revoked {
			cmd.Printf("  - %s (revoked at: %s)\n", keyID, revokedTime.Format(time.RFC3339))

View on GitHub (pinned to 93e97f4bf1)

Solutions

  1. Check the file is JSON and has the expected shape: jq '{signature, timestamp, key_id}' rl.json.sig
  2. If corrupted, regenerate the pair by re-running create/extend-revocation-list with the root private key
  3. Ensure the key_id value is exactly 16 hex characters
  4. Never hand-edit signature files — they are only meaningful as produced by the signer

Example fix

# before: rl.json.sig contains an HTML error page
jq . rl.json.sig   # parse error

# after: regenerate the matched pair
signer extend-revocation-list --key-id 1a2b3c4d5e6f7080 --revocation-list-file rl.json --private-root-key root.pem
Defensive patterns

Strategy: validation

Validate before calling

func looksLikeSignature(data []byte) bool {
    var v map[string]json.RawMessage
    return json.Unmarshal(data, &v) == nil &&
        len(v["signature"]) > 0 &&
        len(v["timestamp"]) > 0 &&
        len(v["key_id"]) > 0
}

// sig, err := os.ReadFile(signatureFile)
// if err == nil && !looksLikeSignature(sig) { /* wrong or corrupt .sig */ }

Type guard

func isSignatureJSON(data []byte) bool {
    var v struct {
        Signature json.RawMessage `json:"signature"`
        Timestamp json.RawMessage `json:"timestamp"`
        KeyID     json.RawMessage `json:"key_id"`
    }
    return json.Unmarshal(data, &v) == nil &&
        len(v.Signature) > 0 && len(v.Timestamp) > 0 && len(v.KeyID) > 0
}

Prevention

When it happens

Trigger: A binary or garbage .sig file; a signature JSON edited so base64 or timestamps no longer unmarshal; a key_id field rewritten to a non-16-hex string; feeding the list JSON into --signature-file (fails downstream, not here).

Common situations: Signature file corrupted in transfer (CRLF mangling, HTML error page saved as .sig); hand-crafted signature files; tooling that re-serializes JSON with different field types.

Understand the failure class

Related errors


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