netbirdio/netbird · critical

revocation list verification failed

Error message

revocation list verification failed

What it means

Returned by the revocation-list verification path (client/internal/updater/reposign/revocation.go:150) when verifyAny fails to verify the reconstructed signed message (revocation list data || little-endian timestamp) against any trusted public root key. The revocation list is what rejects compromised updater keys, so an unverifiable list is rejected outright rather than treated as empty; this check fails closed by design.

Source

Thrown at client/internal/updater/reposign/revocation.go:150

	}

	// Validate signature timestamp is close to LastUpdated
	// (prevents signing old lists with new timestamps)
	timeDiff := signature.Timestamp.Sub(revoList.LastUpdated).Abs()
	if timeDiff > maxClockSkew {
		err := fmt.Errorf("signature timestamp %v differs too much from list LastUpdated %v (diff: %v)",
			signature.Timestamp, revoList.LastUpdated, timeDiff)
		log.Errorf("timestamp mismatch in revocation list: %v", err)
		return nil, err
	}

	// Reconstruct the signed message: revocation_list_data || timestamp || version
	msg := make([]byte, 0, len(data)+8)
	msg = append(msg, data...)
	msg = binary.LittleEndian.AppendUint64(msg, uint64(signature.Timestamp.Unix()))

	if !verifyAny(publicRootKeys, msg, signature.Signature) {
		return nil, errors.New("revocation list verification failed")
	}
	return revoList, nil
}

func CreateRevocationList(privateRootKey RootKey, expiration time.Duration) ([]byte, []byte, error) {
	now := time.Now()
	rl := RevocationList{
		Revoked:     make(map[KeyID]time.Time),
		LastUpdated: now.UTC(),
		ExpiresAt:   now.Add(expiration).UTC(),
	}

	signature, err := signRevocationList(privateRootKey, rl)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to sign revocation list: %w", err)
	}

	rlData, err := json.Marshal(&rl)

View on GitHub (pinned to 93e97f4bf1)

Solutions

  1. Update the NetBird client to the latest release so its embedded root keys can verify the current revocation list.
  2. Clear any cached revocation list artifact and re-download from the official source, then retry.
  3. If it persists, do not skip revocation checking; report it to the NetBird maintainers via the security policy.

Example fix

// before: ignoring revocation verification failure and proceeding
list, err := reposign.VerifyRevocationList(data, sig)
if err != nil {
    log.Warnf("revocation check failed, assuming no revocations") // NEVER
}

// after: fail closed
list, err := reposign.VerifyRevocationList(data, sig)
if err != nil {
    return fmt.Errorf("refusing update, revocation list unverifiable: %w", err)
}
Defensive patterns

Strategy: try-catch

Try / catch

list, err := reposign.VerifyRevocationList(data, sig)
if err != nil {
    // Fail closed: an unverifiable revocation list must not be treated as
    // 'no revocations'. Abort the update and surface the error.
    return fmt.Errorf("update aborted, revocation list untrusted: %w", err)
}

Prevention

When it happens

Trigger: The revocation list was signed by a rotated root key unknown to this client build; the list file is corrupted or truncated so the signed bytes differ; the list was tampered with (attacker stripping revocations). Note the timestamp-vs-LastUpdated skew check runs earlier, so reaching this line means the signature itself does not verify.

Common situations: Old client builds after root-key rotation; cached/stale revocation list from a mirror; intercepted or corrupted downloads. Because accepting an unverified list would let revoked keys through, the updater refuses to continue with it.

Related errors


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