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 nilView on GitHub (pinned to 93e97f4bf1)
Solutions
- 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
- If truncated or empty, restore the last signed copy from backup or artifact storage
- If the file was hand-edited, revert — edits invalidate the signature anyway; use extend-revocation-list instead
- 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
- Distinguish files by content, not suffix: the list has revoked/last_updated/expires_at, the .sig has signature/timestamp/key_id
- Never hand-edit the revocation list; always extend through the signer
- jq-validate the file after every transfer between hosts
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
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- failed to create revocation list: %w
- failed to read revocation list file: %w
- failed to extend revocation list: %w
- failed to parse signature: %w
- failed to write output files: %w
AI-assisted analysis of netbirdio/netbird@93e97f4bf1 (2026-08-16).
Data as JSON: /api/errors/31989dd89a842ffe.
Report an issue: GitHub.