golang/go · error
tls: failed to sign handshake: {err}
Error message
tls: failed to sign handshake: {err} What it means
Thrown during the TLS 1.3 client handshake when signing the CertificateVerify message fails. The client calls crypto.SignMessage with its private key (RSA-PSS, ECDSA, or Ed25519) over the transcript hash, and the underlying signer returned an error. This is a local signing failure, not a network or peer problem.
Source
Thrown at src/crypto/tls/handshake_client_tls13.go:802
// CertificateRequestInfo supported signature algorithms.
c.sendAlert(alertHandshakeFailure)
return err
}
sigType, sigHash, err := typeAndHashFromSignatureScheme(certVerifyMsg.signatureAlgorithm)
if err != nil {
return c.sendAlert(alertInternalError)
}
signed := signedMessage(clientSignatureContext, hs.transcript)
signOpts := crypto.SignerOpts(sigHash)
if sigType == signatureRSAPSS {
signOpts = &rsa.PSSOptions{SaltLength: rsa.PSSSaltLengthEqualsHash, Hash: sigHash}
}
sig, err := crypto.SignMessage(cert.PrivateKey.(crypto.Signer), c.config.rand(), signed, signOpts)
if err != nil {
c.sendAlert(alertInternalError)
return errors.New("tls: failed to sign handshake: " + err.Error())
}
certVerifyMsg.signature = sig
if _, err := hs.c.writeHandshakeRecord(certVerifyMsg, hs.transcript); err != nil {
return err
}
return nil
}
func (hs *clientHandshakeStateTLS13) sendClientFinished() error {
c := hs.c
finished := &finishedMsg{
verifyData: hs.suite.finishedHash(c.out.trafficSecret, hs.transcript),
}
if _, err := hs.c.writeHandshakeRecord(finished, hs.transcript); err != nil {View on GitHub (pinned to b6b368adc5)
Solutions
- Verify the configured tls.Certificate.PrivateKey implements crypto.Signer (all standard *rsa.PrivateKey, *ecdsa.PrivateKey, and ed25519.PrivateKey values do).
- Ensure the private key was loaded with the correct parser (x509.ParsePKCS1PrivateKey / ParsePKCS8PrivateKey / ParseECPrivateKey) and matches the certificate's public key algorithm.
- If the key lives in an HSM or PKCS#11 module, check that the token is present, the session is authorized, and the mechanism (RSA-PSS with PSSSaltLengthEqualsHash) is permitted by token policy.
- Log the wrapped err (err.Error() is concatenated into the message) to identify whether it is a crypto/rsa, crypto/ecdsa, or signer-type error.
Example fix
// before
cert, err := tls.LoadX509KeyPair("client.crt", "client.key")
if err != nil { return err }
// client.key was an encrypted PEM; PrivateKey came back nil/invalid
// after
decryptPEM, err := decryptBlock(pemBlock, password)
cert, err := tls.X509KeyPair(certPEM, decryptPEM)
if err != nil { return err }
if _, ok := cert.PrivateKey.(crypto.Signer); !ok {
return errors.New("private key does not implement crypto.Signer")
} Defensive patterns
Strategy: try-catch
Validate before calling
// Before dialing, ensure the client cert key can sign.
if cert, ok := tlsCert.PrivateKey.(crypto.Signer); ok {
// Optionally dry-run a signature over random bytes with the same
// opts you expect TLS to use.
h := sha256.Sum256([]byte("probe"))
if _, err := cert.Sign(rand.Reader, h[:], crypto.SHA256); err != nil {
return fmt.Errorf("private key unusable: %w", err)
}
} else {
return errors.New("private key does not implement crypto.Signer")
} Type guard
func isCryptoSigner(k any) bool {
_, ok := k.(crypto.Signer)
return ok
} Try / catch
// Wrap tls.Dial / http.Client.Do and inspect the error string.
conn, err := tls.Dial("tcp", addr, cfg)
if err != nil && strings.Contains(err.Error(), "failed to sign handshake") {
// surface as a credential/key problem, not a network error
return fmt.Errorf("client key signing failed: %w", err)
} Prevention
- Always load key pairs with tls.X509KeyPair / LoadX509KeyPair and check the returned error.
- For HSM-backed keys, run a preflight Sign probe during startup, not at first request.
- Assert cert.PrivateKey.(crypto.Signer) at config time, not handshake time.
When it happens
Trigger: clientHandshakeStateTLS13.sendClientCertificateVerify invokes crypto.Signer.Sign via crypto.SignMessage with cert.PrivateKey; failure occurs when the PrivateKey does not implement crypto.Signer, the key is corrupt/unreadable, an HSM/PKCS#11 backing the key rejects the operation, or RSA-PSS salt length / hash parameters are incompatible with the key.
Common situations: Loading a certificate with a private key that omits Sign (e.g. a raw *rsa.PrivateKey wrapped incorrectly), a hardware token yanked mid-handshake, an HSM policy blocking the requested hash, or feeding a key parsed without the matching Parse function so its internal state is incomplete.
Understand the failure class
- SSL/TLS and certificate errors — how TLS handshakes and certificate validation fail.
Related errors
- tls: ML-DSA certificates require TLS 1.3
- tls: EncryptedClientHelloConfigList contains no valid config
- tls: server sent an unnecessary HelloRetryRequest key_share
- tls: internal error: supportsCurve accepted unimplemented cu
- tls: received a session ticket with invalid lifetime
AI-assisted analysis of golang/go@b6b368adc5 (2026-08-12).
Data as JSON: /api/errors/dfacab79b7a6d620.
Report an issue: GitHub.