netbirdio/netbird · error

failed to parse revocation list: %w

Error message

failed to parse revocation list: %w

What it means

reposign.ParseRevocationList rejected the file contents (client/internal/updater/reposign/revocation.go:67). It json.Unmarshal's into RevocationList — whose custom UnmarshalJSON parses every key of the revoked map with ParseKeyID (16 hex chars) — then requires non-zero last_updated and expires_at. Failure modes: 'failed to unmarshal revocation list' (empty, truncated, or malformed JSON, or wrong field types), 'failed to parse KeyID %q' (a revoked entry key is not 16 hex chars), 'revocation list missing last_updated timestamp', and 'revocation list missing expires_at timestamp'.

Source

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

func handleExtendRevocationList(cmd *cobra.Command, keyID, revocationListFile, 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, err := os.ReadFile(revocationListFile)
	if err != nil {
		return fmt.Errorf("failed to read revocation list file: %w", err)
	}

	rl, err := reposign.ParseRevocationList(rlBytes)
	if err != nil {
		return fmt.Errorf("failed to parse revocation list: %w", err)
	}

	kid, err := reposign.ParseKeyID(keyID)
	if err != nil {
		return fmt.Errorf("invalid key ID: %w", err)
	}

	newRLBytes, sigBytes, err := reposign.ExtendRevocationList(*privateRootKey, *rl, kid, expirationDuration)
	if err != nil {
		return fmt.Errorf("failed to extend revocation list: %w", err)
	}

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

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

View on GitHub (pinned to 93e97f4bf1)

Solutions

  1. Confirm the file is the list, not the signature: it must contain top-level revoked, last_updated, and expires_at keys — check with jq . rl.json
  2. If truncated or empty, restore the last signed copy from backup or artifact storage
  3. If the file was hand-edited, revert — edits invalidate the signature anyway; use extend-revocation-list instead
  4. If unrecoverable, recreate with create-revocation-list and re-apply each revocation via extend-revocation-list

Example fix

# before
signer extend-revocation-list --key-id 1a2b... --revocation-list-file rl.json.sig --private-root-key root.pem
# error: failed to parse revocation list: revocation list missing last_updated timestamp

# after
signer extend-revocation-list --key-id 1a2b... --revocation-list-file rl.json --private-root-key root.pem
Defensive patterns

Strategy: validation

Validate before calling

func looksLikeRevocationList(data []byte) error {
    if !json.Valid(data) {
        return fmt.Errorf("not valid JSON")
    }
    var v struct {
        Revoked     map[string]time.Time `json:"revoked"`
        LastUpdated *time.Time           `json:"last_updated"`
        ExpiresAt   *time.Time           `json:"expires_at"`
    }
    if err := json.Unmarshal(data, &v); err != nil {
        return fmt.Errorf("shape mismatch: %w", err)
    }
    if v.LastUpdated == nil || v.ExpiresAt == nil {
        return fmt.Errorf("missing last_updated or expires_at")
    }
    for k := range v.Revoked {
        if !keyIDRe.MatchString(k) {
            return fmt.Errorf("revoked key %q is not 16 hex chars", k)
        }
    }
    return nil
}

Type guard

func isRevocationListJSON(data []byte) bool {
    var v struct {
        LastUpdated *time.Time `json:"last_updated"`
        ExpiresAt   *time.Time `json:"expires_at"`
    }
    return json.Valid(data) &&
        json.Unmarshal(data, &v) == nil &&
        v.LastUpdated != nil && v.ExpiresAt != nil
}

Prevention

When it happens

Trigger: Pointing --revocation-list-file at the .sig sidecar (signature JSON has no last_updated/expires_at, so the timestamp checks fail); a truncated file after an interrupted write or transfer; a hand-edited list whose JSON is broken; a file where a revoked entry key was edited to something non-hex.

Common situations: Swapping the list and signature arguments; editing the JSON to revoke a key manually (breaks both JSON validity and the signature); partially downloaded list from artifact storage; an empty file created by touch.

Understand the failure class

Related errors


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