{"record":{"id":"b1ce270afe980829","repo":"netbirdio/netbird","slug":"failed-to-validate-revocation-list-w","errorCode":null,"errorMessage":"failed to validate revocation list: %w","messagePattern":"failed to validate revocation list: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"client/cmd/signer/revocation.go","lineNumber":193,"sourceCode":"\t\treturn fmt.Errorf(\"failed to read public root key file: %w\", err)\n\t}\n\n\t// Parse public root key\n\tpublicKey, err := reposign.ParseRootPublicKey(pubKeyPEM)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to parse public root key: %w\", err)\n\t}\n\n\t// Parse signature\n\tsignature, err := reposign.ParseSignature(sigBytes)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to parse signature: %w\", err)\n\t}\n\n\t// Validate revocation list\n\trl, err := reposign.ValidateRevocationList([]reposign.PublicKey{publicKey}, rlBytes, *signature)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to validate revocation list: %w\", err)\n\t}\n\n\t// Display results\n\tcmd.Println(\"✅ Revocation list signature is valid\")\n\tcmd.Printf(\"Last Updated: %s\\n\", rl.LastUpdated.Format(time.RFC3339))\n\tcmd.Printf(\"Expires At: %s\\n\", rl.ExpiresAt.Format(time.RFC3339))\n\tcmd.Printf(\"Number of revoked keys: %d\\n\", len(rl.Revoked))\n\n\tif len(rl.Revoked) > 0 {\n\t\tcmd.Println(\"\\nRevoked Keys:\")\n\t\tfor keyID, revokedTime := range rl.Revoked {\n\t\t\tcmd.Printf(\"  - %s (revoked at: %s)\\n\", keyID, revokedTime.Format(time.RFC3339))\n\t\t}\n\t}\n\n\treturn nil\n}\n","sourceCodeStart":175,"sourceCodeEnd":211,"githubUrl":"https://github.com/netbirdio/netbird/blob/93e97f4bf1ad715072dcb3fb6cdb1763431b5a9c/client/cmd/signer/revocation.go#L175-L211","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"# before: list was hand-edited, old .sig reused\nsigner verify-revocation-list --revocation-list-file rl.json --signature-file rl.json.sig --public-root-key root-public.pem\n# error: failed to validate revocation list: signature timestamp ... differs too much from list LastUpdated ...\n\n# after: re-sign properly instead of editing\nsigner extend-revocation-list --key-id 1a2b3c4d5e6f7080 --revocation-list-file rl.json --private-root-key root.pem\nsigner verify-revocation-list --revocation-list-file rl.json --signature-file rl.json.sig --public-root-key root-public.pem","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"rl, err := reposign.ValidateRevocationList([]reposign.PublicKey{publicKey}, rlBytes, *sig)\nif err != nil {\n    switch {\n    case strings.Contains(err.Error(), \"expired at\"):\n        // re-sign with a fresh expiration window via extend-revocation-list\n    case strings.Contains(err.Error(), \"verification failed\"),\n        strings.Contains(err.Error(), \"differs too much\"):\n        // .sig does not cover these bytes: regenerate list+sig as a pair\n    case strings.Contains(err.Error(), \"in the future\"):\n        // clock skew beyond 5m: sync time (NTP), then re-verify\n    default:\n        // parse or structural failure: do not trust or serve the list\n    }\n    return err // fail closed: a failed revocation check never passes\n}","preventionTips":["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"],"tags":["go","cli","ed25519","signature","revocation","clock-skew"],"backgroundTag":null,"analyzedSha":"93e97f4bf1ad715072dcb3fb6cdb1763431b5a9c","analyzedAt":"2026-08-16T03:09:19.136Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}