{"record":{"id":"ace08fbe529f89c5","repo":"golang/go","slug":"tls-internal-error-empty-verified-chain","errorCode":null,"errorMessage":"tls: internal error: empty verified chain","messagePattern":"tls: internal error: empty verified chain","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/crypto/tls/ticket.go","lineNumber":151,"sourceCode":"\t} else {\n\t\tb.AddUint8(0)\n\t}\n\tif s.EarlyData {\n\t\tb.AddUint8(1)\n\t} else {\n\t\tb.AddUint8(0)\n\t}\n\tmarshalCertificate(&b, Certificate{\n\t\tCertificate:                 certificatesToBytesSlice(s.peerCertificates),\n\t\tOCSPStaple:                  s.ocspResponse,\n\t\tSignedCertificateTimestamps: s.scts,\n\t})\n\tb.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) {\n\t\tfor _, chain := range s.verifiedChains {\n\t\t\tb.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) {\n\t\t\t\t// We elide the first certificate because it's always the leaf.\n\t\t\t\tif len(chain) == 0 {\n\t\t\t\t\tb.SetError(errors.New(\"tls: internal error: empty verified chain\"))\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tfor _, cert := range chain[1:] {\n\t\t\t\t\tb.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) {\n\t\t\t\t\t\tb.AddBytes(cert.Raw)\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t})\n\tif s.EarlyData {\n\t\tb.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) {\n\t\t\tb.AddBytes([]byte(s.alpnProtocol))\n\t\t})\n\t}\n\tif s.version >= VersionTLS13 {\n\t\tif s.isClient {\n\t\t\taddUint64(&b, s.useBy)","sourceCodeStart":133,"sourceCodeEnd":169,"githubUrl":"https://github.com/golang/go/blob/b6b368adc57c96c3151d224d172029f233ead2c3/src/crypto/tls/ticket.go#L133-L169","documentation":"Thrown during serialization of a TLS SessionState (ticket.go:151) when one of the entries in s.verifiedChains has length zero. The marshaler elides the leaf cert and writes chain[1:], so an empty chain is structurally invalid. This is a defensive/internal error: a well-formed SessionState produced by the TLS stack always has at least the leaf in each verified chain.","triggerScenarios":"Calling (*SessionState).Bytes() (via SessionState.Bytes / session ticket marshaling) on a SessionState whose verifiedChains slice contains an empty []string/[]*x509.Certificate entry. Most often reached when application code hand-constructs a SessionState and appends an empty chain, or when a custom ClientSessionCache/ServerSessionCache stores a corrupted state.","commonSituations":"Manually building a SessionState for testing or for a custom ticket key rotation scheme; downgrading/transforming session data between Go versions; bugs in resumption code that mutate verifiedChains; importing session blobs from non-Go TLS implementations that omit chain construction.","solutions":["Do not hand-build SessionState.verifiedChains; obtain SessionState from the stack via ClientSessionState/ServerSessionState and round-trip it with Bytes()/ParseSessionState().","If you must construct it, ensure every entry in verifiedChains is non-empty and starts with the leaf certificate (the marshaler elides chain[0]).","Validate verifiedChains before serializing: for each chain check len(chain) > 0 and chain[0] != nil.","If the state came from a cache, treat corruption as a cache miss: drop the ticket and force a full handshake."],"exampleFix":"// before\nss := &tls.SessionState{\n    verifiedChains: [][]*x509.Certificate{{}}, // empty chain -> error\n}\ndata, err := ss.Bytes()\n\n// after\nss := &tls.SessionState{\n    verifiedChains: [][]*x509.Certificate{{leafCert, intermediateCert}},\n}\ndata, err := ss.Bytes()","handlingStrategy":"validation","validationCode":"// Before calling ss.Bytes(), ensure every verified chain is non-empty.\nfunc validVerifiedChains(ss *tls.SessionState) bool {\n    for _, chain := range ss.verifiedChains {\n        if len(chain) == 0 {\n            return false\n        }\n        for _, c := range chain {\n            if c == nil || len(c.Raw) == 0 {\n                return false\n            }\n        }\n    }\n    return true\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Never hand-construct SessionState.verifiedChains; obtain it from the TLS stack.","When round-tripping through a cache, prefer tls.ParseSessionState on retrieval and discard on error.","Treat this 'internal error' as cache corruption — drop the ticket and force a full handshake."],"tags":["tls","session-resumption","serialization","internal-error"],"analyzedSha":"b6b368adc57c96c3151d224d172029f233ead2c3","analyzedAt":"2026-08-12T00:22:02.250Z","schemaVersion":2},"datasetVersion":"2026-08-12T08:17:17.861Z"}