{"record":{"id":"8cd14c2181e35eda","repo":"gotify/server","slug":"w-signature-must-be-d-bytes","errorCode":null,"errorMessage":"%w: signature must be %d bytes","messagePattern":"%w: signature must be (.+?) bytes","errorType":"exception","errorClass":"errCannotParseToken","httpStatus":null,"severity":"error","filePath":"auth/token.go","lineNumber":146,"sourceCode":"\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)\n\tif err := ed25519.VerifyWithOptions(pkOrPubkey, sha512.Sum(nil), signature, &ed25519.Options{Hash: crypto.SHA512}); err != nil {\n\t\treturn nil, errInvalidToken\n\t}\n\treturn &EnhancedToken{\n\t\tident:        ident,\n\t\tpubOrPrivKey: pkOrPubkey,\n\t\ttimestamp:    timestamp,\n\t\tsignature:    signature,\n\t}, nil\n}","sourceCodeStart":128,"sourceCodeEnd":164,"githubUrl":"https://github.com/gotify/server/blob/14bfc256276775c425f988d621dccfe705de18ac/auth/token.go#L128-L164","documentation":"The fourth field of the enhanced token is the base64 (Raw URL encoding) signature. Before decoding, the code checks that the decoded length would equal ed25519.SignatureSize (64 bytes); base64.RawURLEncoding.DecodedLen(len(sigB64)) is derived purely from the encoded length. If it does not match, the token is rejected with errCannotParseToken.","triggerScenarios":"Calling ParseEnhancedToken with a token whose signature field is the wrong length: an empty field, a truncated or padded base64 string (标准 '=' padding breaks Raw encoding length math), or a signature from a different algorithm/key size.","commonSituations":"Copy-paste dropped trailing characters of the signature, '=' padding added by a client that used StdEncoding instead of RawURLEncoding, an old-format token after a signature scheme change, or manual token crafting.","solutions":["Regenerate the token with the server's signer, which emits base64.RawURLEncoding of a 64-byte ed25519 signature (86 characters, no padding).","If generating tokens client-side, encode with base64.RawURLEncoding (not StdEncoding) and confirm the signature is exactly ed25519.SignatureSize bytes.","Check the token string was not truncated; the signature is the last field, so it is most vulnerable to cut-and-paste loss.","Treat errors.Is(err, errCannotParseToken) as an invalid-token condition and force re-authentication."],"exampleFix":"// before (Go client)\nsigB64 := base64.StdEncoding.EncodeToString(sig) // adds '=' padding, wrong DecodedLen\n// after\nsigB64 := base64.RawURLEncoding.EncodeToString(sig) // 64-byte sig -> 86 unpadded chars","handlingStrategy":"validation","validationCode":"parts := strings.Split(token, \".\")\nif len(parts) != 4 {\n    return errors.New(\"token must have 4 fields\")\n}\nsigLen := base64.RawURLEncoding.DecodedLen(len(parts[3]))\nif sigLen != ed25519.SignatureSize {\n    return fmt.Errorf(\"signature field is %d decoded bytes, want %d (is it padded or truncated?)\", sigLen, ed25519.SignatureSize)\n}","typeGuard":"func hasValidSignatureLength(token string) bool {\n    parts := strings.Split(token, \".\")\n    return len(parts) == 4 &&\n        base64.RawURLEncoding.DecodedLen(len(parts[3])) == ed25519.SignatureSize\n}","tryCatchPattern":"parsed, err := ParseEnhancedToken(token)\nif errors.Is(err, errCannotParseToken) {\n    log.Printf(\"token rejected: %v\", err)\n    return nil, errUnauthorized // re-authenticate, never retry same token\n}","preventionTips":["Always encode signatures with base64.RawURLEncoding (86 chars for ed25519, no '=')","Copy tokens with tools that preserve the full string (avoid terminal line-wrap truncation)","Add a client-side length check before sending tokens","Version the token format so clients can detect scheme changes"],"tags":["go","token-parsing","ed25519","base64","auth"],"backgroundTag":"invalid-token-signature","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"}