diff --git a/boring_test.go b/boring_test.go index 085ff57..77374ab 100644 --- a/boring_test.go +++ b/boring_test.go @@ -26,33 +26,42 @@ import ( ) func TestBoringServerProtocolVersion(t *testing.T) { - test := func(name string, v uint16, msg string) { + test := func(t *testing.T, name string, v uint16, msg string) { t.Run(name, func(t *testing.T) { serverConfig := testConfig.Clone() serverConfig.MinVersion = VersionSSL30 - clientHello := &clientHelloMsg{ - vers: v, - random: make([]byte, 32), - cipherSuites: allCipherSuites(), - compressionMethods: []uint8{compressionNone}, - supportedVersions: []uint16{v}, + clientConfig := testConfig.Clone() + clientConfig.MinVersion = v + clientConfig.MaxVersion = v + _, _, err := testHandshake(t, clientConfig, serverConfig) + if msg == "" { + if err != nil { + t.Fatalf("got error: %v, expected success", err) + } + } else { + if err == nil { + t.Fatalf("got success, expected error") + } + if !strings.Contains(err.Error(), msg) { + t.Fatalf("got error %v, expected %q", err, msg) + } } - testClientHelloFailure(t, serverConfig, clientHello, msg) }) } - test("VersionTLS10", VersionTLS10, "") - test("VersionTLS11", VersionTLS11, "") - test("VersionTLS12", VersionTLS12, "") - test("VersionTLS13", VersionTLS13, "") + test(t, "VersionTLS10", VersionTLS10, "") + test(t, "VersionTLS11", VersionTLS11, "") + test(t, "VersionTLS12", VersionTLS12, "") + test(t, "VersionTLS13", VersionTLS13, "") - fipstls.Force() - defer fipstls.Abandon() - test("VersionSSL30", VersionSSL30, "client offered only unsupported versions") - test("VersionTLS10", VersionTLS10, "client offered only unsupported versions") - test("VersionTLS11", VersionTLS11, "client offered only unsupported versions") - test("VersionTLS12", VersionTLS12, "") - test("VersionTLS13", VersionTLS13, "client offered only unsupported versions") + t.Run("fipstls", func(t *testing.T) { + fipstls.Force() + defer fipstls.Abandon() + test(t, "VersionTLS10", VersionTLS10, "supported versions") + test(t, "VersionTLS11", VersionTLS11, "supported versions") + test(t, "VersionTLS12", VersionTLS12, "") + test(t, "VersionTLS13", VersionTLS13, "supported versions") + }) } func isBoringVersion(v uint16) bool { @@ -154,26 +163,22 @@ func TestBoringServerCurves(t *testing.T) { for _, curveid := range defaultCurvePreferences { t.Run(fmt.Sprintf("curve=%d", curveid), func(t *testing.T) { - clientHello := &clientHelloMsg{ - vers: VersionTLS12, - random: make([]byte, 32), - cipherSuites: []uint16{TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256}, - compressionMethods: []uint8{compressionNone}, - supportedCurves: []CurveID{curveid}, - supportedPoints: []uint8{pointFormatUncompressed}, + clientConfig := testConfig.Clone() + clientConfig.CurvePreferences = []CurveID{curveid} + if _, _, err := testHandshake(t, clientConfig, serverConfig); err != nil { + t.Fatalf("got error: %v, expected success", err) } - testClientHello(t, serverConfig, clientHello) - // With fipstls forced, bad curves should be rejected. t.Run("fipstls", func(t *testing.T) { fipstls.Force() defer fipstls.Abandon() - msg := "" - if !isBoringCurve(curveid) { - msg = "no cipher suite supported by both client and server" + _, _, err := testHandshake(t, clientConfig, serverConfig) + if err != nil && isBoringCurve(curveid) { + t.Fatalf("got error: %v, expected success", err) + } else if err == nil && !isBoringCurve(curveid) { + t.Fatalf("got success, expected error") } - testClientHelloFailure(t, serverConfig, clientHello, msg) }) }) } diff --git a/common.go b/common.go index 58dc0c2..34a3013 100644 --- a/common.go +++ b/common.go @@ -299,6 +299,9 @@ type ConnectionState struct { // ekm is a closure exposed via ExportKeyingMaterial. ekm func(label string, context []byte, length int) ([]byte, error) + + // testingOnlyDidHRR is true if a HelloRetryRequest was sent/received. + testingOnlyDidHRR bool } // ExportKeyingMaterial returns length bytes of exported key material in a new diff --git a/conn.go b/conn.go index 30c5f00..c44f651 100644 --- a/conn.go +++ b/conn.go @@ -48,6 +48,7 @@ type Conn struct { handshakes int extMasterSecret bool didResume bool // whether this connection was a session resumption + didHRR bool // whether a HelloRetryRequest was sent/received cipherSuite uint16 ocspResponse []byte // stapled OCSP response scts [][]byte // signed certificate timestamps from server @@ -1608,6 +1609,7 @@ func (c *Conn) connectionStateLocked() ConnectionState { state.Version = c.vers state.NegotiatedProtocol = c.clientProtocol state.DidResume = c.didResume + state.testingOnlyDidHRR = c.didHRR state.NegotiatedProtocolIsMutual = true state.ServerName = c.serverName state.CipherSuite = c.cipherSuite diff --git a/handshake_client_test.go b/handshake_client_test.go index 157c67f..eb0fe36 100644 --- a/handshake_client_test.go +++ b/handshake_client_test.go @@ -659,6 +659,12 @@ func TestHandshakeClientHelloRetryRequest(t *testing.T) { name: "HelloRetryRequest", args: []string{"-cipher", "ECDHE-RSA-AES128-GCM-SHA256", "-curves", "P-256"}, config: config, + validate: func(cs ConnectionState) error { + if !cs.testingOnlyDidHRR { + return errors.New("expected HelloRetryRequest") + } + return nil + }, } runClientTestTLS13(t, test) diff --git a/handshake_client_tls13.go b/handshake_client_tls13.go index bc8670a..88ec383 100644 --- a/handshake_client_tls13.go +++ b/handshake_client_tls13.go @@ -307,6 +307,7 @@ func (hs *clientHandshakeStateTLS13) processHelloRetryRequest() error { return err } + c.didHRR = true return nil } diff --git a/handshake_server_test.go b/handshake_server_test.go index ff0b479..813495d 100644 --- a/handshake_server_test.go +++ b/handshake_server_test.go @@ -52,16 +52,33 @@ func testClientHelloFailure(t *testing.T, serverConfig *Config, m handshakeMessa ctx := context.Background() conn := Server(s, serverConfig) ch, err := conn.readClientHello(ctx) - hs := serverHandshakeState{ - c: conn, - ctx: ctx, - clientHello: ch, - } - if err == nil { - err = hs.processClientHello() - } - if err == nil { - err = hs.pickCipherSuite() + if conn.vers == VersionTLS13 { + hs := serverHandshakeStateTLS13{ + c: conn, + ctx: ctx, + clientHello: ch, + } + if err == nil { + err = hs.processClientHello() + } + if err == nil { + err = hs.checkForResumption() + } + if err == nil { + err = hs.pickCertificate() + } + } else { + hs := serverHandshakeState{ + c: conn, + ctx: ctx, + clientHello: ch, + } + if err == nil { + err = hs.processClientHello() + } + if err == nil { + err = hs.pickCipherSuite() + } } s.Close() if len(expectedSubStr) == 0 { @@ -903,10 +920,52 @@ func TestHandshakeServerHelloRetryRequest(t *testing.T) { name: "HelloRetryRequest", command: []string{"openssl", "s_client", "-no_ticket", "-ciphersuites", "TLS_CHACHA20_POLY1305_SHA256", "-curves", "X25519:P-256"}, config: config, + validate: func(cs ConnectionState) error { + if !cs.testingOnlyDidHRR { + return errors.New("expected HelloRetryRequest") + } + return nil + }, } runServerTestTLS13(t, test) } +// TestHandshakeServerKeySharePreference checks that we prefer a key share even +// if it's later in the CurvePreferences order. +func TestHandshakeServerKeySharePreference(t *testing.T) { + config := testConfig.Clone() + config.CurvePreferences = []CurveID{X25519, CurveP256} + + test := &serverTest{ + name: "KeySharePreference", + command: []string{"openssl", "s_client", "-no_ticket", "-ciphersuites", "TLS_CHACHA20_POLY1305_SHA256", "-curves", "P-256:X25519"}, + config: config, + validate: func(cs ConnectionState) error { + if cs.testingOnlyDidHRR { + return errors.New("unexpected HelloRetryRequest") + } + return nil + }, + } + runServerTestTLS13(t, test) +} + +// TestHandshakeServerUnsupportedKeyShare tests a client that sends a key share +// that's not in the supported groups list. +func TestHandshakeServerUnsupportedKeyShare(t *testing.T) { + pk, _ := ecdh.X25519().GenerateKey(rand.Reader) + clientHello := &clientHelloMsg{ + vers: VersionTLS12, + random: make([]byte, 32), + supportedVersions: []uint16{VersionTLS13}, + cipherSuites: []uint16{TLS_CHACHA20_POLY1305_SHA256}, + compressionMethods: []uint8{compressionNone}, + keyShares: []keyShare{{group: X25519, data: pk.PublicKey().Bytes()}}, + supportedCurves: []CurveID{CurveP256}, + } + testClientHelloFailure(t, testConfig, clientHello, "client sent key share for group it does not support") +} + func TestHandshakeServerALPN(t *testing.T) { config := testConfig.Clone() config.NextProtos = []string{"proto1", "proto2"} @@ -1932,6 +1991,7 @@ func TestAESCipherReorderingTLS13(t *testing.T) { supportedVersions: []uint16{VersionTLS13}, compressionMethods: []uint8{compressionNone}, keyShares: []keyShare{{group: X25519, data: pk.PublicKey().Bytes()}}, + supportedCurves: []CurveID{X25519}, }, } diff --git a/handshake_server_tls13.go b/handshake_server_tls13.go index 7f15d05..a7d3890 100644 --- a/handshake_server_tls13.go +++ b/handshake_server_tls13.go @@ -14,6 +14,7 @@ import ( "hash" "internal/byteorder" "io" + "slices" "time" ) @@ -181,21 +182,25 @@ func (hs *serverHandshakeStateTLS13) processClientHello() error { // groups with a key share, to avoid a HelloRetryRequest round-trip. var selectedGroup CurveID var clientKeyShare *keyShare -GroupSelection: - for _, preferredGroup := range c.config.curvePreferences() { - for _, ks := range hs.clientHello.keyShares { - if ks.group == preferredGroup { - selectedGroup = ks.group - clientKeyShare = &ks - break GroupSelection + preferredGroups := c.config.curvePreferences() + for _, preferredGroup := range preferredGroups { + ki := slices.IndexFunc(hs.clientHello.keyShares, func(ks keyShare) bool { + return ks.group == preferredGroup + }) + if ki != -1 { + clientKeyShare = &hs.clientHello.keyShares[ki] + selectedGroup = clientKeyShare.group + if !slices.Contains(hs.clientHello.supportedCurves, selectedGroup) { + c.sendAlert(alertIllegalParameter) + return errors.New("tls: client sent key share for group it does not support") } + break } - if selectedGroup != 0 { - continue - } - for _, group := range hs.clientHello.supportedCurves { - if group == preferredGroup { - selectedGroup = group + } + if selectedGroup == 0 { + for _, preferredGroup := range preferredGroups { + if slices.Contains(hs.clientHello.supportedCurves, preferredGroup) { + selectedGroup = preferredGroup break } } @@ -532,6 +537,7 @@ func (hs *serverHandshakeStateTLS13) doHelloRetryRequest(selectedGroup CurveID) return errors.New("tls: client illegally modified second ClientHello") } + c.didHRR = true hs.clientHello = clientHello return nil } diff --git a/handshake_test.go b/handshake_test.go index e365a79..f5e467b 100644 --- a/handshake_test.go +++ b/handshake_test.go @@ -296,6 +296,8 @@ Dialing: case c2 := <-localListener.ch: if c2.RemoteAddr().String() == c1.LocalAddr().String() { + t.Cleanup(func() { c1.Close() }) + t.Cleanup(func() { c2.Close() }) return c1, c2 } t.Logf("localPipe: unexpected connection: %v != %v", c2.RemoteAddr(), c1.LocalAddr()) @@ -399,7 +401,7 @@ func runMain(m *testing.M) int { func testHandshake(t *testing.T, clientConfig, serverConfig *Config) (serverState, clientState ConnectionState, err error) { const sentinel = "SENTINEL\n" c, s := localPipe(t) - errChan := make(chan error) + errChan := make(chan error, 1) go func() { cli := Client(c, clientConfig) err := cli.Handshake() @@ -408,7 +410,7 @@ func testHandshake(t *testing.T, clientConfig, serverConfig *Config) (serverStat c.Close() return } - defer cli.Close() + defer func() { errChan <- nil }() clientState = cli.ConnectionState() buf, err := io.ReadAll(cli) if err != nil { @@ -417,7 +419,9 @@ func testHandshake(t *testing.T, clientConfig, serverConfig *Config) (serverStat if got := string(buf); got != sentinel { t.Errorf("read %q from TLS connection, but expected %q", got, sentinel) } - errChan <- nil + if err := cli.Close(); err != nil { + t.Errorf("failed to call cli.Close: %v", err) + } }() server := Server(s, serverConfig) err = server.Handshake() @@ -429,11 +433,11 @@ func testHandshake(t *testing.T, clientConfig, serverConfig *Config) (serverStat if err := server.Close(); err != nil { t.Errorf("failed to call server.Close: %v", err) } - err = <-errChan } else { + err = fmt.Errorf("server: %v", err) s.Close() - <-errChan } + err = errors.Join(err, <-errChan) return } diff --git a/testdata/Server-TLSv13-KeySharePreference b/testdata/Server-TLSv13-KeySharePreference new file mode 100644 index 0000000..fc79cc6 --- /dev/null +++ b/testdata/Server-TLSv13-KeySharePreference @@ -0,0 +1,101 @@ +>>> Flow 1 (client to server) +00000000 16 03 01 00 e5 01 00 00 e1 03 03 58 2d 84 f2 4f |...........X-..O| +00000010 6d 10 9d f6 5a 67 c7 92 78 e6 7d 96 96 fe db 37 |m...Zg..x.}....7| +00000020 07 44 e3 2e 85 3f 1a 5f f1 2a 24 20 cb e8 e8 c4 |.D...?._.*$ ....| +00000030 e9 28 01 21 9f 82 d2 da 6b 3e a0 7f 0d 20 95 12 |.(.!....k>... ..| +00000040 39 5b 3f f8 04 86 df 9b 72 3b 74 e2 00 04 13 03 |9[?.....r;t.....| +00000050 00 ff 01 00 00 94 00 0b 00 04 03 00 01 02 00 0a |................| +00000060 00 06 00 04 00 17 00 1d 00 16 00 00 00 17 00 00 |................| +00000070 00 0d 00 1e 00 1c 04 03 05 03 06 03 08 07 08 08 |................| +00000080 08 09 08 0a 08 0b 08 04 08 05 08 06 04 01 05 01 |................| +00000090 06 01 00 2b 00 03 02 03 04 00 2d 00 02 01 01 00 |...+......-.....| +000000a0 33 00 47 00 45 00 17 00 41 04 63 03 f6 90 df 1f |3.G.E...A.c.....| +000000b0 d0 03 96 24 21 3d e4 c0 5d 38 5f 53 85 a4 9c d8 |...$!=..]8_S....| +000000c0 f7 b0 9c 64 5f 53 c3 66 9b f5 e0 5a 60 0b b2 e8 |...d_S.f...Z`...| +000000d0 2b d0 6a a2 ff 1d 07 8b 5f a8 37 e4 74 35 15 b1 |+.j....._.7.t5..| +000000e0 06 de 79 3b f4 69 52 2a ad 66 |..y;.iR*.f| +>>> Flow 2 (server to client) +00000000 16 03 03 00 9b 02 00 00 97 03 03 00 00 00 00 00 |................| +00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000020 00 00 00 00 00 00 00 00 00 00 00 20 cb e8 e8 c4 |........... ....| +00000030 e9 28 01 21 9f 82 d2 da 6b 3e a0 7f 0d 20 95 12 |.(.!....k>... ..| +00000040 39 5b 3f f8 04 86 df 9b 72 3b 74 e2 13 03 00 00 |9[?.....r;t.....| +00000050 4f 00 2b 00 02 03 04 00 33 00 45 00 17 00 41 04 |O.+.....3.E...A.| +00000060 1e 18 37 ef 0d 19 51 88 35 75 71 b5 e5 54 5b 12 |..7...Q.5uq..T[.| +00000070 2e 8f 09 67 fd a7 24 20 3e b2 56 1c ce 97 28 5e |...g..$ >.V...(^| +00000080 f8 2b 2d 4f 9e f1 07 9f 6c 4b 5b 83 56 e2 32 42 |.+-O....lK[.V.2B| +00000090 e9 58 b6 d7 49 a6 b5 68 1a 41 03 56 6b dc 5a 89 |.X..I..h.A.Vk.Z.| +000000a0 14 03 03 00 01 01 17 03 03 00 17 f8 6a 35 d1 d7 |............j5..| +000000b0 a0 09 48 20 66 ea 1d 4e a4 05 2b d6 24 ea 0d cb |..H f..N..+.$...| +000000c0 68 8b 17 03 03 02 6d 16 c5 3b c2 80 85 49 51 1f |h.....m..;...IQ.| +000000d0 4e 0e 8a 11 63 95 9b 83 46 a9 19 43 90 ee 55 ae |N...c...F..C..U.| +000000e0 4f 09 ae cf fd 64 dd a9 c0 af 31 fd 95 29 20 9f |O....d....1..) .| +000000f0 da d4 27 97 2c da da 2c c8 e7 e5 c2 6b 24 5f dd |..'.,..,....k$_.| +00000100 68 86 b1 9b 01 95 8a 78 bb 10 26 66 24 d4 59 df |h......x..&f$.Y.| +00000110 6d b4 56 c4 23 56 5d 3c fc 6d 69 ce eb e2 9c 30 |m.V.#V]<.mi....0| +00000120 da 34 39 32 80 a7 26 d6 e2 11 33 a5 c1 5d 75 07 |.492..&...3..]u.| +00000130 f5 f6 98 9e f2 26 49 96 52 fb 56 6d 34 67 45 15 |.....&I.R.Vm4gE.| +00000140 97 0d 0d 5e 7e 63 6b 42 0e 6f 2c 3b 11 06 d5 a4 |...^~ckB.o,;....| +00000150 00 92 2d 39 62 ea f4 28 04 ee e4 f4 78 f4 d2 72 |..-9b..(....x..r| +00000160 f7 65 61 43 76 12 59 90 a3 15 71 07 78 dc 61 cf |.eaCv.Y...q.x.a.| +00000170 62 6e 7d 2d 7e b0 0f e3 ca 21 1b 13 96 ef 96 23 |bn}-~....!.....#| +00000180 86 31 2a 53 b0 13 a3 23 29 74 2c 98 3b 75 a2 96 |.1*S...#)t,.;u..| +00000190 7b 4a cb 22 f3 63 84 17 9b 25 a8 10 75 88 3c fd |{J.".c...%..u.<.| +000001a0 d5 49 c3 97 c3 78 83 c1 24 f3 7f 21 63 e8 b2 01 |.I...x..$..!c...| +000001b0 b0 92 b8 8b f2 83 64 2f a8 8f b9 18 14 44 54 6a |......d/.....DTj| +000001c0 a9 d6 68 5c be 50 69 8d 35 16 5e 8d 9a 6a f0 5b |..h\.Pi.5.^..j.[| +000001d0 a4 f1 5b 51 3e 5b d4 d1 41 06 3c c7 09 27 96 a8 |..[Q>[..A.<..'..| +000001e0 81 07 d2 33 27 3c a6 a9 bb 7b 80 28 58 e4 b0 dd |...3'<...{.(X...| +000001f0 a8 9e b2 61 ed 5b 57 c8 b2 a2 7f 6b f7 0f ee 3d |...a.[W....k...=| +00000200 9c 6d a1 76 55 3e af aa 17 c8 a8 ec c6 14 31 77 |.m.vU>........1w| +00000210 38 a3 9c 15 3f 0e cc 63 43 8f b0 c1 14 97 ee 85 |8...?..cC.......| +00000220 30 08 b1 95 0c 7f 5b 95 4d 4f 18 26 d8 45 cb b4 |0.....[.MO.&.E..| +00000230 b7 1b f5 a5 d5 e5 21 da 53 88 57 09 ed 30 fe d1 |......!.S.W..0..| +00000240 ac ee c7 80 ee a9 96 31 92 4c a1 e2 2c a8 f5 b1 |.......1.L..,...| +00000250 d9 3c bd c8 e6 1b ba 7c 91 d6 6c d4 ae 0a 15 50 |.<.....|..l....P| +00000260 b9 24 80 14 ff 28 98 94 a7 7f d4 13 16 1f 03 bf |.$...(..........| +00000270 72 83 94 a3 8a 6d b6 d7 b2 c7 22 56 38 f2 15 e1 |r....m...."V8...| +00000280 ea 1f 78 d0 ff ac c4 19 54 a1 c6 2d 8a da cd f3 |..x.....T..-....| +00000290 6e 45 b3 a4 dc e3 a0 6e f8 18 af a6 c6 20 ce a7 |nE.....n..... ..| +000002a0 eb dc 42 06 bd d4 bf a0 ef 36 4c f6 38 42 3d f7 |..B......6L.8B=.| +000002b0 a3 a5 ac 4d b3 71 36 9b 00 ee 1f 40 fa bc b3 d4 |...M.q6....@....| +000002c0 5b 49 79 4f 16 fd 3e 4f ab 8e cc 92 7d f3 1d c7 |[IyO..>O....}...| +000002d0 13 76 49 56 1c 59 13 56 3b 6b 33 ed 1a 85 9c b7 |.vIV.Y.V;k3.....| +000002e0 a1 55 84 83 a1 df d6 53 0c c1 b6 63 63 b2 58 84 |.U.....S...cc.X.| +000002f0 12 f6 99 7a ac c5 ee 53 69 9e 86 76 88 aa 7b 2f |...z...Si..v..{/| +00000300 f8 48 0b 05 e1 2a 1c 0d 56 ae 79 9e 68 4f b5 85 |.H...*..V.y.hO..| +00000310 df cc 11 05 33 94 55 e6 16 d4 d5 78 b4 d1 c3 2c |....3.U....x...,| +00000320 2d 3c ac 45 6e fd 1e e0 79 5c 23 c3 57 66 3e d2 |-<.En...y\#.Wf>.| +00000330 22 39 21 df 17 03 03 00 99 a9 e3 ac d5 82 cc bd |"9!.............| +00000340 74 c3 92 13 4d 32 fc ff e4 63 ec ea 81 40 47 bb |t...M2...c...@G.| +00000350 3f ad 65 d4 fe 4f 0c c0 6a b0 78 c4 4f 0d e8 73 |?.e..O..j.x.O..s| +00000360 7c ee 9b ff 61 f8 4b 17 32 92 5d e5 49 ea 7b 38 ||...a.K.2.].I.{8| +00000370 6b db a2 4b 64 1d 7f 42 ce 4d f0 d8 dc 9c 93 f4 |k..Kd..B.M......| +00000380 23 5d d5 dd 34 90 51 42 3d 0e bf 69 31 d8 0f e6 |#]..4.QB=..i1...| +00000390 14 5a ec 52 1e d5 41 a6 25 7c 9b 40 f5 92 58 17 |.Z.R..A.%|.@..X.| +000003a0 80 68 a2 43 58 d3 d9 7c 7a 1f 90 4f a5 08 f8 0f |.h.CX..|z..O....| +000003b0 31 7b bd cc 74 3d f8 73 7d 0a 6c 12 25 53 b3 99 |1{..t=.s}.l.%S..| +000003c0 c7 f2 4c 99 5d 7c 56 cd 29 5c 30 91 93 6f a2 00 |..L.]|V.)\0..o..| +000003d0 c4 97 17 03 03 00 35 81 b3 9e 92 c5 dc 08 b3 f3 |......5.........| +000003e0 70 56 52 40 16 d4 75 34 1e e8 b7 ce 5e 5a 7e 47 |pVR@..u4....^Z~G| +000003f0 ec d0 75 68 24 06 a6 1b 87 73 6c b0 ff e4 3c 6d |..uh$....sl......kOC.`.U.u...| +00000460 27 64 8b 0f 15 db 95 47 36 b3 14 e0 da a2 21 1e |'d.....G6.....!.| +00000470 bf 0d 40 79 c8 6c 3b 9f eb 96 1a cf 9d 09 f7 a9 |..@y.l;.........| +00000480 22 e5 13 c4 2a 69 5e 95 d1 40 e5 00 26 20 ae 16 |"...*i^..@..& ..| +00000490 55 27 51 a1 c7 e3 2e 1d 32 6c d5 0d |U'Q.....2l..| +>>> Flow 3 (client to server) +00000000 14 03 03 00 01 01 17 03 03 00 35 71 16 57 df 09 |..........5q.W..| +00000010 d7 73 e4 f2 c4 8a 10 b1 d8 73 dc 5b 87 8f 56 51 |.s.......s.[..VQ| +00000020 6a ed 61 66 c9 d4 0d fe 28 0c 6f c7 4d ef e6 90 |j.af....(.o.M...| +00000030 ba e2 fb c3 c9 4a 94 a2 e5 7f 23 e2 66 2b 4f 9e |.....J....#.f+O.| +>>> Flow 4 (server to client) +00000000 17 03 03 00 1e 6a c8 6d 0d b6 f7 c8 33 cd c6 25 |.....j.m....3..%| +00000010 98 0e bb ac de 69 61 9d ec a3 c0 be 7e 53 44 cb |.....ia.....~SD.| +00000020 1f d5 97 17 03 03 00 13 be 18 cc 16 91 88 1e d1 |................| +00000030 b5 7c 58 17 fb 39 b2 80 76 7b a8 |.|X..9..v{.|