gotify/server · error · errCannotParseToken

%w: public key must be %d bytes

Error message

%w: public key must be %d bytes

What it means

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.

Source

Thrown at auth/token.go:136

	}
	ident := fields[0]
	pkOrPubkeyB64 := fields[1]
	pkOrPubkeyBytesLen := base64.RawURLEncoding.DecodedLen(len(pkOrPubkeyB64))
	pkOrPubkey, err := base64.RawURLEncoding.DecodeString(pkOrPubkeyB64)
	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)

View on GitHub (pinned to 14bfc25627)

Solutions

  1. Encode the ed25519 public key with base64.RawURLEncoding (43 chars for 32 bytes).
  2. Confirm the value in the pubkey slot is the public key, not the seed or signature.
  3. Regenerate the token via PublicForm()/String() to guarantee correct structure.
  4. If you only have the private seed, use the 2-field token form instead.

Example fix

// before: signature in pubkey slot
tok := "gtfy" + ident + "." + b64(signature) + "." + challenge + "." + sig
// after: public key in pubkey slot
tok := "gtfy" + ident + "." + b64(pubKey) + "." + challenge + "." + sig
Defensive patterns

Strategy: validation

Validate before calling

fields := strings.Split(strings.TrimPrefix(tok, "gtfy"), ".")
if len(fields) == 4 && base64.RawURLEncoding.DecodedLen(len(fields[1])) != ed25519.PublicKeySize {
    return errors.New("4-field token pubkey must decode to 32 bytes")
}

Type guard

func isEd25519PubKeySegment(seg string) bool {
    return len(seg) == 43 && base64.RawURLEncoding.DecodedLen(len(seg)) == ed25519.PublicKeySize
}

Try / catch

if _, err := auth.ParseEnhancedToken(raw); err != nil {
    if strings.Contains(err.Error(), "public key must be") {
        return fmt.Errorf("pubkey slot holds wrong material: %w", err)
    }
}

Prevention

When it happens

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

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

Related errors


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