netbirdio/netbird · error
failed to validate revocation list: %w
Error message
failed to validate revocation list: %w
What it means
reposign.ValidateRevocationList rejected the list (client/internal/updater/reposign/revocation.go:89). It enforces, in order: the list parses; signature.Timestamp is not in the future beyond 5 minutes of clock skew; the signature is not older than 10 years; list LastUpdated is not in the future; the list has not expired (now > ExpiresAt); ExpiresAt is not beyond 10 years; |signature.Timestamp - LastUpdated| <= 5 minutes; and Ed25519 verification of data||little-endian-timestamp against every supplied root public key. Each check returns a distinct error, ending with the generic 'revocation list verification failed' when the signature math itself does not check out.
Source
Thrown at client/cmd/signer/revocation.go:193
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))
}
}
return nil
}
View on GitHub (pinned to 93e97f4bf1)
Solutions
- Read the exact sub-error in the %w chain before acting
- 'differs too much from list LastUpdated' or 'revocation list verification failed': the .sig does not cover these exact bytes — regenerate it by re-running extend-revocation-list so list and signature are produced together
- 'revocation list expired at ...': re-sign with a fresh window — extend refreshes ExpiresAt using the --expiration flag (default 1 year)
- Wrong-key verification failure: pass the public key matching the root that signed (check the RootKey[ID=...] printed at creation)
- 'in the future' errors: sync the machine clock (NTP) and re-verify; never serve or trust a list that fails validation
Example fix
# before: list was hand-edited, old .sig reused signer verify-revocation-list --revocation-list-file rl.json --signature-file rl.json.sig --public-root-key root-public.pem # error: failed to validate revocation list: signature timestamp ... differs too much from list LastUpdated ... # after: re-sign properly instead of editing signer extend-revocation-list --key-id 1a2b3c4d5e6f7080 --revocation-list-file rl.json --private-root-key root.pem signer verify-revocation-list --revocation-list-file rl.json --signature-file rl.json.sig --public-root-key root-public.pem
Defensive patterns
Strategy: try-catch
Try / catch
rl, err := reposign.ValidateRevocationList([]reposign.PublicKey{publicKey}, rlBytes, *sig)
if err != nil {
switch {
case strings.Contains(err.Error(), "expired at"):
// re-sign with a fresh expiration window via extend-revocation-list
case strings.Contains(err.Error(), "verification failed"),
strings.Contains(err.Error(), "differs too much"):
// .sig does not cover these bytes: regenerate list+sig as a pair
case strings.Contains(err.Error(), "in the future"):
// clock skew beyond 5m: sync time (NTP), then re-verify
default:
// parse or structural failure: do not trust or serve the list
}
return err // fail closed: a failed revocation check never passes
} Prevention
- Always mutate the list through extend-revocation-list so list and .sig are regenerated together
- Never hand-edit list JSON — the signature covers the exact bytes
- Run verify-revocation-list immediately after every sign and again before publishing
- Keep public/private root key pairs matched and labeled through rotations
- Sync host clocks; validation only tolerates 5 minutes of skew
- Watch ExpiresAt (default 1 year) and refresh before expiry
When it happens
Trigger: A .sig from a different revision of the list (edited or extended without regenerating the signature — timestamp mismatch or verify failure); verifying with a public key that does not match the root key that signed (rotation mismatch); a list past its ExpiresAt (default 365 days); local clock off by more than 5 minutes; signature bytes or list bytes altered after signing.
Common situations: Hand-editing the list JSON; mixing files from two signing runs; key rotation without republishing the list; an old list left in an artifact repository past expiry; verifying on a VM with a stale clock.
Related errors
- failed to read signature file: %w
- revocation list verification failed
- failed to parse artifact private key: %w
- failed to read revocation list file: %w
- failed to parse revocation list: %w
AI-assisted analysis of netbirdio/netbird@93e97f4bf1 (2026-08-16).
Data as JSON: /api/errors/b1ce270afe980829.
Report an issue: GitHub.