gotify/server · error · errCannotParseToken

%w: signature must be %d bytes

Error message

%w: signature must be %d bytes

What it means

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.

Source

Thrown at auth/token.go:146

			return nil, fmt.Errorf("%w: private key must be %d bytes", errCannotParseToken, ed25519.SeedSize)
		}
		return &EnhancedToken{
			ident:        ident,
			pubOrPrivKey: pkOrPubkey,
		}, nil
	}
	if pkOrPubkeyBytesLen != ed25519.PublicKeySize {
		return nil, fmt.Errorf("%w: public key must be %d bytes", errCannotParseToken, ed25519.PublicKeySize)
	}
	timestampStr := fields[2]
	timestamp, err := strconv.ParseInt(timestampStr, 10, 64)
	if err != nil {
		return nil, fmt.Errorf("%w: timestamp must be an integer: %w", errCannotParseToken, err)
	}
	signatureB64 := fields[3]
	signatureBytesLen := base64.RawURLEncoding.DecodedLen(len(signatureB64))
	if signatureBytesLen != ed25519.SignatureSize {
		return nil, fmt.Errorf("%w: signature must be %d bytes", errCannotParseToken, ed25519.SignatureSize)
	}
	signature, err := base64.RawURLEncoding.DecodeString(signatureB64)
	if err != nil {
		return nil, fmt.Errorf("%w: base64 decode failed: %w", errCannotParseToken, err)
	}
	sha512 := sha512.New()
	sha512.Write([]byte("iat=")) // query-like encoding to give us some semantic headroom should we need more fields in the future
	fmt.Fprintf(sha512, "%d", timestamp)
	if err := ed25519.VerifyWithOptions(pkOrPubkey, sha512.Sum(nil), signature, &ed25519.Options{Hash: crypto.SHA512}); err != nil {
		return nil, errInvalidToken
	}
	return &EnhancedToken{
		ident:        ident,
		pubOrPrivKey: pkOrPubkey,
		timestamp:    timestamp,
		signature:    signature,
	}, nil
}

View on GitHub (pinned to 14bfc25627)

Solutions

  1. Regenerate the token with the server's signer, which emits base64.RawURLEncoding of a 64-byte ed25519 signature (86 characters, no padding).
  2. If generating tokens client-side, encode with base64.RawURLEncoding (not StdEncoding) and confirm the signature is exactly ed25519.SignatureSize bytes.
  3. Check the token string was not truncated; the signature is the last field, so it is most vulnerable to cut-and-paste loss.
  4. Treat errors.Is(err, errCannotParseToken) as an invalid-token condition and force re-authentication.

Example fix

// before (Go client)
sigB64 := base64.StdEncoding.EncodeToString(sig) // adds '=' padding, wrong DecodedLen
// after
sigB64 := base64.RawURLEncoding.EncodeToString(sig) // 64-byte sig -> 86 unpadded chars
Defensive patterns

Strategy: validation

Validate before calling

parts := strings.Split(token, ".")
if len(parts) != 4 {
    return errors.New("token must have 4 fields")
}
sigLen := base64.RawURLEncoding.DecodedLen(len(parts[3]))
if sigLen != ed25519.SignatureSize {
    return fmt.Errorf("signature field is %d decoded bytes, want %d (is it padded or truncated?)", sigLen, ed25519.SignatureSize)
}

Type guard

func hasValidSignatureLength(token string) bool {
    parts := strings.Split(token, ".")
    return len(parts) == 4 &&
        base64.RawURLEncoding.DecodedLen(len(parts[3])) == ed25519.SignatureSize
}

Try / catch

parsed, err := ParseEnhancedToken(token)
if errors.Is(err, errCannotParseToken) {
    log.Printf("token rejected: %v", err)
    return nil, errUnauthorized // re-authenticate, never retry same token
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of gotify/server@14bfc25627 (2026-09-05). Data as JSON: /api/errors/8cd14c2181e35eda. Report an issue: GitHub.