{"record":{"id":"2b07fc43d4a970d3","repo":"gotify/server","slug":"w-public-key-must-be-d-bytes","errorCode":null,"errorMessage":"%w: public key must be %d bytes","messagePattern":"%w: public key must be (.+?) bytes","errorType":"exception","errorClass":"errCannotParseToken","httpStatus":null,"severity":"error","filePath":"auth/token.go","lineNumber":136,"sourceCode":"\t}\n\tident := fields[0]\n\tpkOrPubkeyB64 := fields[1]\n\tpkOrPubkeyBytesLen := base64.RawURLEncoding.DecodedLen(len(pkOrPubkeyB64))\n\tpkOrPubkey, err := base64.RawURLEncoding.DecodeString(pkOrPubkeyB64)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"%w: base64 decode failed: %w\", errCannotParseToken, err)\n\t}\n\tif len(fields) == 2 {\n\t\tif pkOrPubkeyBytesLen != ed25519.SeedSize {\n\t\t\treturn nil, fmt.Errorf(\"%w: private key must be %d bytes\", errCannotParseToken, ed25519.SeedSize)\n\t\t}\n\t\treturn &EnhancedToken{\n\t\t\tident:        ident,\n\t\t\tpubOrPrivKey: pkOrPubkey,\n\t\t}, nil\n\t}\n\tif pkOrPubkeyBytesLen != ed25519.PublicKeySize {\n\t\treturn nil, fmt.Errorf(\"%w: public key must be %d bytes\", errCannotParseToken, ed25519.PublicKeySize)\n\t}\n\ttimestampStr := fields[2]\n\ttimestamp, err := strconv.ParseInt(timestampStr, 10, 64)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"%w: timestamp must be an integer: %w\", errCannotParseToken, err)\n\t}\n\tsignatureB64 := fields[3]\n\tsignatureBytesLen := base64.RawURLEncoding.DecodedLen(len(signatureB64))\n\tif signatureBytesLen != ed25519.SignatureSize {\n\t\treturn nil, fmt.Errorf(\"%w: signature must be %d bytes\", errCannotParseToken, ed25519.SignatureSize)\n\t}\n\tsignature, err := base64.RawURLEncoding.DecodeString(signatureB64)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"%w: base64 decode failed: %w\", errCannotParseToken, err)\n\t}\n\tsha512 := sha512.New()\n\tsha512.Write([]byte(\"iat=\")) // query-like encoding to give us some semantic headroom should we need more fields in the future\n\tfmt.Fprintf(sha512, \"%d\", timestamp)","sourceCodeStart":118,"sourceCodeEnd":154,"githubUrl":"https://github.com/gotify/server/blob/14bfc256276775c425f988d621dccfe705de18ac/auth/token.go#L118-L154","documentation":"For the 4-field (public-key) form, the second field must decode to exactly ed25519.PublicKeySize (32 bytes). Otherwise ParseEnhancedToken returns errCannotParseToken wrapped with \"public key must be 32 bytes\", because token verification needs a valid ed25519 public key.","triggerScenarios":"Parsing a \"gtfyident.<pubkey>.<challenge>.<signature>\" token where the pubkey field decodes to a length other than 32 bytes — e.g. a seed/signature pasted in the pubkey slot, or a corrupted/truncated key segment.","commonSituations":"Manual token assembly with the wrong key slot; copy/paste dropping characters from the 43-char base64 segment; encoding the key with hex or padded base64 before insertion.","solutions":["Encode the ed25519 public key with base64.RawURLEncoding (43 chars for 32 bytes).","Confirm the value in the pubkey slot is the public key, not the seed or signature.","Regenerate the token via PublicForm()/String() to guarantee correct structure.","If you only have the private seed, use the 2-field token form instead."],"exampleFix":"// before: signature in pubkey slot\ntok := \"gtfy\" + ident + \".\" + b64(signature) + \".\" + challenge + \".\" + sig\n// after: public key in pubkey slot\ntok := \"gtfy\" + ident + \".\" + b64(pubKey) + \".\" + challenge + \".\" + sig","handlingStrategy":"validation","validationCode":"fields := strings.Split(strings.TrimPrefix(tok, \"gtfy\"), \".\")\nif len(fields) == 4 && base64.RawURLEncoding.DecodedLen(len(fields[1])) != ed25519.PublicKeySize {\n    return errors.New(\"4-field token pubkey must decode to 32 bytes\")\n}","typeGuard":"func isEd25519PubKeySegment(seg string) bool {\n    return len(seg) == 43 && base64.RawURLEncoding.DecodedLen(len(seg)) == ed25519.PublicKeySize\n}","tryCatchPattern":"if _, err := auth.ParseEnhancedToken(raw); err != nil {\n    if strings.Contains(err.Error(), \"public key must be\") {\n        return fmt.Errorf(\"pubkey slot holds wrong material: %w\", err)\n    }\n}","preventionTips":["Put the ed25519 public key — never a seed or signature — in the pubkey slot.","Serialize 4-field tokens with PublicForm() to guarantee slot ordering.","Check the base64 segment is 43 characters before parsing."],"tags":["auth","token","ed25519","crypto"],"backgroundTag":"invalid-token-format","analyzedSha":"14bfc256276775c425f988d621dccfe705de18ac","analyzedAt":"2026-09-05T12:52:36.781Z","contentChangedAt":"2026-09-05T12:52:36.781Z","schemaVersion":2},"datasetVersion":"2026-09-12T17:17:11.597Z"}