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 = trueView on GitHub (pinned to b6b368adc5)
Solutions
- Ensure all servers in the pool share the same session ticket key (tls.Config.SessionTicketKey, or SetSessionTicketKeys for rotation)
- During ticket-key rotation, keep the old key for an overlap window so in-flight tickets still validate
- 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
- Share session ticket keys across all servers in a pool
- During rotation, call SetSessionTicketKeys with both old and new keys for an overlap window
- Clients should request a fresh ticket when resumption fails
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
- SSL/TLS and certificate errors — how TLS handshakes and certificate validation fail.
Related errors
- tls: server sent unrequested session ticket
- tls: server selected an invalid PSK
- tls: server selected an invalid PSK and cipher suite pair
- tls: invalid or missing PSK binders
- tls: missing signature_algorithms from TLS 1.2 peer
AI-assisted analysis of golang/go@b6b368adc5 (2026-08-12).
Data as JSON: /api/errors/48745b53510a60bf.
Report an issue: GitHub.