{"record":{"id":"31989dd89a842ffe","repo":"netbirdio/netbird","slug":"failed-to-parse-revocation-list-w-31989d","errorCode":null,"errorMessage":"failed to parse revocation list: %w","messagePattern":"failed to parse revocation list: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"client/cmd/signer/revocation.go","lineNumber":138,"sourceCode":"func handleExtendRevocationList(cmd *cobra.Command, keyID, revocationListFile, privateRootKeyFile string) error {\n\tprivKeyPEM, err := os.ReadFile(privateRootKeyFile)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to read private root key file: %w\", err)\n\t}\n\n\tprivateRootKey, err := reposign.ParseRootKey(privKeyPEM)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to parse private root key: %w\", err)\n\t}\n\n\trlBytes, err := os.ReadFile(revocationListFile)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to read revocation list file: %w\", err)\n\t}\n\n\trl, err := reposign.ParseRevocationList(rlBytes)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to parse revocation list: %w\", err)\n\t}\n\n\tkid, err := reposign.ParseKeyID(keyID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"invalid key ID: %w\", err)\n\t}\n\n\tnewRLBytes, sigBytes, err := reposign.ExtendRevocationList(*privateRootKey, *rl, kid, expirationDuration)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to extend revocation list: %w\", err)\n\t}\n\n\tif err := writeOutputFiles(revocationListFile, revocationListFile+\".sig\", newRLBytes, sigBytes); err != nil {\n\t\treturn fmt.Errorf(\"failed to write output files: %w\", err)\n\t}\n\n\tcmd.Println(\"✅ Revocation list extended successfully\")\n\treturn nil","sourceCodeStart":120,"sourceCodeEnd":156,"githubUrl":"https://github.com/netbirdio/netbird/blob/93e97f4bf1ad715072dcb3fb6cdb1763431b5a9c/client/cmd/signer/revocation.go#L120-L156","documentation":"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'.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"# before\nsigner extend-revocation-list --key-id 1a2b... --revocation-list-file rl.json.sig --private-root-key root.pem\n# error: failed to parse revocation list: revocation list missing last_updated timestamp\n\n# after\nsigner extend-revocation-list --key-id 1a2b... --revocation-list-file rl.json --private-root-key root.pem","handlingStrategy":"validation","validationCode":"func looksLikeRevocationList(data []byte) error {\n    if !json.Valid(data) {\n        return fmt.Errorf(\"not valid JSON\")\n    }\n    var v struct {\n        Revoked     map[string]time.Time `json:\"revoked\"`\n        LastUpdated *time.Time           `json:\"last_updated\"`\n        ExpiresAt   *time.Time           `json:\"expires_at\"`\n    }\n    if err := json.Unmarshal(data, &v); err != nil {\n        return fmt.Errorf(\"shape mismatch: %w\", err)\n    }\n    if v.LastUpdated == nil || v.ExpiresAt == nil {\n        return fmt.Errorf(\"missing last_updated or expires_at\")\n    }\n    for k := range v.Revoked {\n        if !keyIDRe.MatchString(k) {\n            return fmt.Errorf(\"revoked key %q is not 16 hex chars\", k)\n        }\n    }\n    return nil\n}","typeGuard":"func isRevocationListJSON(data []byte) bool {\n    var v struct {\n        LastUpdated *time.Time `json:\"last_updated\"`\n        ExpiresAt   *time.Time `json:\"expires_at\"`\n    }\n    return json.Valid(data) &&\n        json.Unmarshal(data, &v) == nil &&\n        v.LastUpdated != nil && v.ExpiresAt != nil\n}","tryCatchPattern":null,"preventionTips":["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"],"tags":["go","cli","json","revocation","reposign"],"backgroundTag":null,"analyzedSha":"93e97f4bf1ad715072dcb3fb6cdb1763431b5a9c","analyzedAt":"2026-08-16T03:09:19.136Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}