golang/go · error

tls: invalid PSK binder

Error message

tls: invalid PSK binder

What it means

The PSK binder is an HMAC over the ClientHello transcript with a key derived from the PSK (RFC 8446 §4.4.2). If it doesn't match the server's recomputation, the offered PSK identity is wrong or the transcript was tampered with; the server sends decrypt_error.

Source

Thrown at src/crypto/tls/handshake_server_tls13.go:406

		hs.earlySecret = tls13.NewEarlySecret(hs.suite.hash.New, sessionState.secret)
		binderKey := hs.earlySecret.ResumptionBinderKey()
		// Clone the transcript in case a HelloRetryRequest was recorded.
		transcript := cloneHash(hs.transcript, hs.suite.hash)
		if transcript == nil {
			c.sendAlert(alertInternalError)
			return errors.New("tls: internal error: failed to clone hash")
		}
		clientHelloBytes, err := hs.clientHello.marshalWithoutBinders()
		if err != nil {
			c.sendAlert(alertInternalError)
			return err
		}
		transcript.Write(clientHelloBytes)
		pskBinder := hs.suite.finishedHash(binderKey, transcript)
		if !hmac.Equal(hs.clientHello.pskBinders[i], pskBinder) {
			c.sendAlert(alertDecryptError)
			return errors.New("tls: invalid PSK binder")
		}

		if c.quic != nil && hs.clientHello.earlyData && i == 0 &&
			sessionState.EarlyData && sessionState.cipherSuite == hs.suite.id &&
			sessionState.alpnProtocol == c.clientProtocol {
			hs.earlyData = true

			transcript := hs.suite.hash.New()
			if err := transcriptMsg(hs.clientHello, transcript); err != nil {
				return err
			}
			earlyTrafficSecret := hs.earlySecret.ClientEarlyTrafficSecret(transcript)
			if err := c.quicSetReadSecret(QUICEncryptionLevelEarly, hs.suite.id, earlyTrafficSecret); err != nil {
				return err
			}
		}

		c.didResume = true

View on GitHub (pinned to b6b368adc5)

Solutions

  1. Ensure all servers in the pool share the same session ticket key (tls.Config.SessionTicketKey, or SetSessionTicketKeys for rotation)
  2. During ticket-key rotation, keep the old key for an overlap window so in-flight tickets still validate
  3. Have the client request a fresh ticket when resumption fails (fall back to a full handshake)

Example fix

// Shared ticket key across pool (all servers must agree)
var key [32]byte
// ... populate key securely, distribute to every server ...
cfg.SessionTicketKey = key

// For rotation, use SetSessionTicketKeys with old+new during the overlap:
cfg.SetSessionTicketKeys([][32]byte{newKey, oldKey})
Defensive patterns

Strategy: validation

Validate before calling

// Ensure every server in the pool uses the same ticket key.
var ticketKey [32]byte
// ... distribute ticketKey to all servers via a secure channel ...
cfg.SessionTicketKey = ticketKey

Try / catch

// On the server this is a peer/ticket error; let the client fall back to a full handshake.
if err := tlsConn.Handshake(); err != nil {
    if strings.Contains(err.Error(), "invalid PSK binder") {
        // Common during ticket-key rotation; usually transient.
        log.Printf("PSK binder mismatch from %v (ticket-key rotation?)", remote)
    }
    c.Close()
    return
}

Prevention

When it happens

Trigger: Client offers a session ticket whose binder fails HMAC verification: stale/expired ticket, ticket from a server with different ticket keys, corrupted ticket, or a transcript modified in flight.

Common situations: Server pool with mismatched session ticket keys; ticket-key rotation without overlap; client reusing a cached ticket long after issuance; middleboxes modifying the ClientHello bytes.

Understand the failure class

Related errors


AI-assisted analysis of golang/go@b6b368adc5 (2026-08-12). Data as JSON: /api/errors/48745b53510a60bf. Report an issue: GitHub.