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

  1. Read the exact sub-error in the %w chain before acting
  2. '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
  3. 'revocation list expired at ...': re-sign with a fresh window — extend refreshes ExpiresAt using the --expiration flag (default 1 year)
  4. Wrong-key verification failure: pass the public key matching the root that signed (check the RootKey[ID=...] printed at creation)
  5. '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

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


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