gotify/server · error · errCannotParseToken

%w: timestamp must be an integer: %w

Error message

%w: timestamp must be an integer: %w

What it means

ParseEnhancedToken parses a dot-separated enhanced token of the form pubkey.pk.timestamp.signature. Field 3 (index 2) is the issuance timestamp and must parse as a base-10 int64. If strconv.ParseInt fails, the token is rejected and the error is wrapped in errCannotParseToken so callers can errors.Is/As against it.

Source

Thrown at auth/token.go:141

	if err != nil {
		return nil, fmt.Errorf("%w: base64 decode failed: %w", errCannotParseToken, err)
	}
	if len(fields) == 2 {
		if pkOrPubkeyBytesLen != ed25519.SeedSize {
			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,

View on GitHub (pinned to 14bfc25627)

Solutions

  1. Regenerate the token on the server with the current signing code so the timestamp field is a plain base-10 int64 (unix seconds).
  2. Verify the token has exactly the expected number of dot-separated fields and that field index 2 is the timestamp; do not reorder fields.
  3. Check the token was not truncated or mangled in transit (trailing whitespace stripped, no URL-encoding of dots).
  4. Handle the wrapped errCannotParseToken in the caller and prompt the user to re-authenticate instead of retrying.

Example fix

// before
token := "pk.pk. 1757000000.sig" // timestamp field contains a space
parsed, err := ParseEnhancedToken(token)
// after
token := "pk.pk.1757000000.sig" // field 2 is a clean int64
parsed, err := ParseEnhancedToken(token)
if errors.Is(err, errCannotParseToken) { /* re-authenticate */ }
Defensive patterns

Strategy: validation

Validate before calling

parts := strings.Split(token, ".")
if len(parts) != 4 {
    return fmt.Errorf("token must have 4 dot-separated fields, got %d", len(parts))
}
if _, err := strconv.ParseInt(parts[2], 10, 64); err != nil {
    return fmt.Errorf("timestamp field %q is not an int64", parts[2])
}

Type guard

func isParseableToken(token string) bool {
    parts := strings.Split(token, ".")
    if len(parts) != 4 { return false }
    _, err := strconv.ParseInt(parts[2], 10, 64)
    return err == nil
}

Try / catch

parsed, err := ParseEnhancedToken(token)
if errors.Is(err, errCannotParseToken) {
    // token malformed: force re-auth, do not retry
    return nil, fmt.Errorf("malformed token: %w", err)
}
if err != nil {
    return nil, err // signature verification failed (errInvalidToken)
}

Prevention

When it happens

Trigger: Calling ParseEnhancedToken with a token whose third dot-separated field is not a valid decimal integer (empty field, non-numeric text, float like '1757000000.5', or a value overflowing int64 such as a 20+ digit number).

Common situations: Tokens truncated when copy-pasting, tokens hand-assembled or tampered with, mismatched token format after a library version change that added or reordered fields, or passing a base64/URL-encoded token where the encoding corrupted digits.

Related errors


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