{"record":{"id":"b7b8ad00ccf81ced","repo":"golang/go","slug":"tls-unexpected-switch-in-encrypted-client-hello-e","errorCode":null,"errorMessage":"tls: unexpected switch in encrypted client hello extension type","messagePattern":"tls: unexpected switch in encrypted client hello extension type","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/crypto/tls/handshake_server_tls13.go","lineNumber":607,"sourceCode":"\t\tc.sendAlert(alertUnexpectedMessage)\n\t\treturn nil, unexpectedMessageError(clientHello, msg)\n\t}\n\n\tif hs.echContext != nil {\n\t\tif len(clientHello.encryptedClientHello) == 0 {\n\t\t\tc.sendAlert(alertMissingExtension)\n\t\t\treturn nil, errors.New(\"tls: second client hello missing encrypted client hello extension\")\n\t\t}\n\n\t\techType, echCiphersuite, configID, encap, payload, err := parseECHExt(clientHello.encryptedClientHello)\n\t\tif err != nil {\n\t\t\tc.sendAlert(alertDecodeError)\n\t\t\treturn nil, errors.New(\"tls: client sent invalid encrypted client hello extension\")\n\t\t}\n\n\t\tif echType == outerECHExt && hs.echContext.inner || echType == innerECHExt && !hs.echContext.inner {\n\t\t\tc.sendAlert(alertDecodeError)\n\t\t\treturn nil, errors.New(\"tls: unexpected switch in encrypted client hello extension type\")\n\t\t}\n\n\t\tif echType == outerECHExt {\n\t\t\tif echCiphersuite != hs.echContext.ciphersuite || configID != hs.echContext.configID || len(encap) != 0 {\n\t\t\t\tc.sendAlert(alertIllegalParameter)\n\t\t\t\treturn nil, errors.New(\"tls: second client hello encrypted client hello extension does not match\")\n\t\t\t}\n\n\t\t\tencodedInner, err := decryptECHPayload(hs.echContext.hpkeContext, clientHello.original, payload)\n\t\t\tif err != nil {\n\t\t\t\tc.sendAlert(alertDecryptError)\n\t\t\t\treturn nil, errors.New(\"tls: failed to decrypt second client hello encrypted client hello extension payload\")\n\t\t\t}\n\n\t\t\techInner, err := decodeInnerClientHello(clientHello, encodedInner)\n\t\t\tif err != nil {\n\t\t\t\tc.sendAlert(alertIllegalParameter)\n\t\t\t\treturn nil, errors.New(\"tls: client sent invalid encrypted client hello extension\")","sourceCodeStart":589,"sourceCodeEnd":625,"githubUrl":"https://github.com/golang/go/blob/b6b368adc57c96c3151d224d172029f233ead2c3/src/crypto/tls/handshake_server_tls13.go#L589-L625","documentation":"ECH has an outer (encrypted) and inner (decrypted) extension type. The server tracks via hs.echContext.inner whether it is processing the inner or outer ClientHello. If the second ClientHello flips type inconsistently (outer when inner expected, or vice versa), it violates the ECH flow and the server sends decode_error.","triggerScenarios":"ClientHello1 was one ECH type, ClientHello2 is the opposite, contradicting the established echContext. E.g., outer in CH1 but inner in CH2.","commonSituations":"ECH client bug, a man-in-the-middle modifying the ECH type, fuzzers.","solutions":["Keep the ECH extension type consistent between ClientHello1 and ClientHello2 as required by the ECH draft","Update the ECH client implementation to handle HRR consistently"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"if err := tlsConn.Handshake(); err != nil {\n    if strings.Contains(err.Error(), \"unexpected switch in encrypted client hello extension type\") {\n        log.Printf(\"ECH type flip on retry from %v (possible MITM)\", remote)\n    }\n    c.Close()\n    return\n}","preventionTips":["Keep the ECH extension type (outer/inner) consistent across ClientHello1 and ClientHello2","Treat a type flip as suspicious — it can indicate tampering"],"tags":["tls","go","ech","encrypted-client-hello","handshake","draft"],"analyzedSha":"b6b368adc57c96c3151d224d172029f233ead2c3","analyzedAt":"2026-08-12T00:22:02.250Z","schemaVersion":2},"datasetVersion":"2026-08-12T08:17:17.861Z"}