Repository: pion/dtls
Pull request #369 (2021-05-20 09:25:25)

Stateful session resumption
Add session resumption support.

You could use the [examples/listen/psk/main.go](https://github.com/pion/dtls/blob/v2.0.9/examples/listen/psk/main.go) and openssl to test this feature.

First you need  to change the server's config and run the server.
```go
    config := &dtls.Config{
          ...
          CipherSuites: []dtls.CipherSuiteID{dtls.TLS_PSK_WITH_AES_128_GCM_SHA256},
          SessionStore: ...,
          ServerName: "example.com",
          ...
    }
```

And then, run openssl one time to store the session data.
```
openssl s_client -dtls -cipher PSK-AES128-GCM-SHA256 -psk abc123 -sess_out /tmp/s.txt  -connect 127.0.0.1:4444
```

Finally, run openssl in the resumption mode.
```
openssl s_client -dtls -cipher PSK-AES128-GCM-SHA256 -psk abc123 -sess_in /tmp/s.txt  -connect 127.0.0.1:4444
```

daenney: RFC 5077 is indeed obsolete since RFC 8446. But the RFC that obsoleted it is about TLS 1.3, whereas we support (D)TLS 1.2. So generally speaking supporting session resumption based on session ID is not unreasonable for us to have. DTLS 1.3 also still has some support for it in the form of the [`legacy_session_id` in `ClientHello`](https://www.ietf.org/archive/id/draft-ietf-tls-dtls13-43.html#name-clienthello-message) even though it has a new way to do session resumption.

With respect to your PR: we can't accept this addition of functionality without tests that verify it works as intended. This will also ensure we don't break this feature in the future. This is especially important for features we don't expect to be used/exercised regularly by users of the library.

On a personal note; we have made it this far without it, and I am a little frustrated by the idea of supporting this b/c a company with resources like Cisco can't be bothered to follow an RFC and implement PSK support. However, that should not stop us from supporting session resumption in general.
Sean-Der: Very cool project! Would love to help get this merged :) 100% agree with @daenney we need tests.

Also DTLS resumption has been known to cause security issues so want to be especially careful with this change!
lvht: this pr just make some basic feature which will be needed to implement session resume. If it will be accepted, i will try my best to add the test case.
daenney: I don't have strong objections to it. I'd be inclined to accept the PR seeing it purely implements session resumption.
lvht: Hi @daenney @Sean-Der  
I have implemented session resumption. Please make your comments. Thank you.

This PR has a small test. And it could work with openssl's dtls session resumption.
lvht: Anybody is here?
daenney: We're here. But please keep in mind we are open source maintainers, doing this work in our free time. We can't always respond within hours, and review will take time.
lvht: > We're here. But please keep in mind we are open source maintainers, doing this work in our free time. We can't always respond within hours, and review will take time.

Hi @daenney 
There are nobody expecting hourly respond. I do understand most contributors make contributions in their free time.

However, I made this PR three days ago and asked yesterday. I just want to make sure there are maintainers who have interest in this PR. And it will be great encouragement to me if some maintainers could make some response or put this MR review in their plan.

Thank you.
pf512: When I add SessionStore to the config in the [https://github.com/pion/dtls/blob/v2.0.9/examples/listen/psk/main.go](url) example    

`config := &dtls.Config{
          ...
+         SessionStore: &dtls.FileSessionStore{Root: "/any/exist/path/to/store/session"},
          ...
    }
`

I get a nil pointer error in the client when I try to connect to server.

`Server's hint: Pion DTLS Client 
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x2 addr=0x30 pc=0x10429855c]

goroutine 19 [running]:
github.com/pion/dtls/v2.flight5Parse(0x104346658, 0x1400015b5c0, 0x1043473f0, 0x14000170000, 0x14000170040, 0x1400014c2c0, 0x14000172000, 0x1, 0x0, 0x10429ccd8, ...)
        /Users/test/dtls/flight5handler.go:58 +0x48c
github.com/pion/dtls/v2.(*handshakeFSM).wait(0x1400010e370, 0x104346658, 0x1400015b5c0, 0x1043473f0, 0x14000170000, 0x3, 0x0, 0x0)
        /Users/test/dtls/handshaker.go:268 +0x1fc
github.com/pion/dtls/v2.(*handshakeFSM).Run(0x1400010e370, 0x104346658, 0x1400015b5c0, 0x1043473f0, 0x14000170000, 0x1, 0x0, 0x0)
        /Users/test/dtls/handshaker.go:178 +0x34c
github.com/pion/dtls/v2.(*Conn).handshake.func2(0x14000170000, 0x104346658, 0x1400015b5c0, 0x101, 0x140001362a0)
        /Users/test/dtls/conn.go:819 +0x80
created by github.com/pion/dtls/v2.(*Conn).handshake
        /Users/test/dtls/conn.go:817 +0x228
`

lvht: @pf512 https://github.com/pion/dtls/pull/369/commits/94a8dfdd778f41d408e316e497347cb2ea0bd971 will fix the error you mentationed.
lvht: Hi @daenney @Sean-Der , one month passed. Would this PR going to be placed in any maintainer schedule?
grokker001: This PR only implements the session ID based session resumption which requires the server to save the session info (https://datatracker.ietf.org/doc/html/rfc5246#appendix-F.1.4). It doesn't implement the session ticket based session resumption: https://datatracker.ietf.org/doc/html/rfc5077
lvht: @grokker001 

This PR is originally designed for Cisco's AnyConnect VPN protocol, which used session resumption to establish DTLS session. And according @daenney 's advice, the full stateful session resumption has been implemented.

If the ticket based session resumption feature is required, it may be a little better to implement it in a dedicate PR. This depends the maintainers requirement.

However, it seems no maintainer has time to make a review or give any feedback. It need to wait the maintainers' bandwidth.
grokker001: How does this PR prevent data replay attack of the resumed "ClientHello" packet?
lvht: @grokker001 
The attacker cannot get the master secret, so the session will terminated by the following code:
https://github.com/pion/dtls/pull/369/files#diff-4f427d2b022907c552328e63f137561f6de92396d7a6e8f6c2ea1bcf0db52654R686-R688
lvht: Hi @daenney @Sean-Der , would you like to offer any feedback about this PR. Even a rejection is welcome.
lvht: @daenney thank you for reply. Waiting for your free time.
epii1: Hi @daenney almost 6 months has past. I not intend to press you make action. But would you like put this MR into your schedule? Or make any clue for your following plan?

Thanks.
daenney: I honestly don't know. I'm not particularly invested in getting session support in here myself since I don't have any need for it in practice. It's also a big change and once we accept it something we'll need to keep supporting going forwards.

Idk if other maintainers like @Sean-Der, @at-wat or @backkem might have time to review this.

Coincidentally, the tests for this thing haven't run yet. Can you rebase and push this again and see if that unblocks it?


epii1: > Coincidentally, the tests for this thing haven't run yet. Can you rebase and push this again and see if that unblocks it?

It seems the workflows will not run without approval. @daenney 
at-wat: I'll review the code tomorrow
daenney: > > Coincidentally, the tests for this thing haven't run yet. Can you rebase and push this again and see if that unblocks it?
> 
> It seems the workflows will not run without approval. @daenney

Yeah, but for some reason GitHub wasn't showing the approval buttons. It did after you pushed, so I've OKed them to run now.
epii1: @daenney @at-wat I will fix the workflow as soon as possible.
at-wat: Refs for reviewing:
- Message flow on session resumption: https://datatracker.ietf.org/doc/html/rfc5246#page-36
- Session ID in ClientHello: https://datatracker.ietf.org/doc/html/rfc5246#page-41
- Session ID in ServerHello: https://datatracker.ietf.org/doc/html/rfc5246#page-43
- Appendix F.  Security Analysis: Resuming Sessions: https://datatracker.ietf.org/doc/html/rfc5246#appendix-F.1.4
grokker001: CVE-2016-5419 and CVE-2017-7468 are very interesting cases.
epii1: Hi @at-wat , thank you for your kindness of detailed review and feedback.
epii1: > CVEs related to session resumption: https://gist.github.com/at-wat/942ab9c2ec8cbae146fcec5d2934272e (@Sean-Der @daenney If you know any other vulnerabilities related to this, please let me know)

@at-wat What about just delete the stored session when the client send its certificate? So the client cannot resume session with such id.
epii1: Hi @at-wat , please run the workflows again. I make sure all the test cases has been passed, finally.
codecov[bot]: # [Codecov](https://codecov.io/gh/pion/dtls/pull/369?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion) Report
> Merging [#369](https://codecov.io/gh/pion/dtls/pull/369?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion) (c50f51a) into [master](https://codecov.io/gh/pion/dtls/commit/f0a790ceb311db23be90b77398fc8ab9b08527df?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion) (f0a790c) will **decrease** coverage by `10.15%`.
> The diff coverage is `67.50%`.

[![Impacted file tree graph](https://codecov.io/gh/pion/dtls/pull/369/graphs/tree.svg?width=650&height=150&src=pr&token=bcCB2jB88I&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion)](https://codecov.io/gh/pion/dtls/pull/369?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion)

```diff
@@             Coverage Diff             @@
##           master     #369       +/-   ##
===========================================
- Coverage   75.91%   65.75%   -10.16%     
===========================================
  Files          88       64       -24     
  Lines        3687     3367      -320     
===========================================
- Hits         2799     2214      -585     
- Misses        603      901      +298     
+ Partials      285      252       -33     
```

| Flag | Coverage Δ | |
|---|---|---|
| go | `?` | |
| wasm | `65.75% <67.50%> (-0.12%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/pion/dtls/pull/369?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion) | Coverage Δ | |
|---|---|---|
| [config.go](https://codecov.io/gh/pion/dtls/pull/369/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion#diff-Y29uZmlnLmdv) | `81.81% <0.00%> (-18.19%)` | :arrow_down: |
| [flight5handler.go](https://codecov.io/gh/pion/dtls/pull/369/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion#diff-ZmxpZ2h0NWhhbmRsZXIuZ28=) | `78.31% <0.00%> (-3.42%)` | :arrow_down: |
| [handshaker.go](https://codecov.io/gh/pion/dtls/pull/369/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion#diff-aGFuZHNoYWtlci5nbw==) | `69.44% <ø> (-4.87%)` | :arrow_down: |
| [flight4handler.go](https://codecov.io/gh/pion/dtls/pull/369/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion#diff-ZmxpZ2h0NGhhbmRsZXIuZ28=) | `78.60% <16.66%> (-4.95%)` | :arrow_down: |
| [conn.go](https://codecov.io/gh/pion/dtls/pull/369/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion#diff-Y29ubi5nbw==) | `77.62% <20.00%> (-4.78%)` | :arrow_down: |
| [flight3handler.go](https://codecov.io/gh/pion/dtls/pull/369/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion#diff-ZmxpZ2h0M2hhbmRsZXIuZ28=) | `74.84% <59.64%> (-11.94%)` | :arrow_down: |
| [pkg/protocol/handshake/message\_client\_hello.go](https://codecov.io/gh/pion/dtls/pull/369/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion#diff-cGtnL3Byb3RvY29sL2hhbmRzaGFrZS9tZXNzYWdlX2NsaWVudF9oZWxsby5nbw==) | `55.38% <60.00%> (-2.52%)` | :arrow_down: |
| [flight0handler.go](https://codecov.io/gh/pion/dtls/pull/369/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion#diff-ZmxpZ2h0MGhhbmRsZXIuZ28=) | `73.01% <61.53%> (-6.99%)` | :arrow_down: |
| [pkg/protocol/handshake/message\_server\_hello.go](https://codecov.io/gh/pion/dtls/pull/369/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion#diff-cGtnL3Byb3RvY29sL2hhbmRzaGFrZS9tZXNzYWdlX3NlcnZlcl9oZWxsby5nbw==) | `58.49% <66.66%> (-1.09%)` | :arrow_down: |
| [flight1handler.go](https://codecov.io/gh/pion/dtls/pull/369/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion#diff-ZmxpZ2h0MWhhbmRsZXIuZ28=) | `88.15% <70.00%> (+0.27%)` | :arrow_up: |
| ... and [75 more](https://codecov.io/gh/pion/dtls/pull/369/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/pion/dtls/pull/369?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/pion/dtls/pull/369?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion). Last update [f0a790c...c50f51a](https://codecov.io/gh/pion/dtls/pull/369?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion).

epii1: hi @at-wat please run workflows again. sorry for disturb.
epii1: hi @at-wat , is there any more problem need i to resolve?
epii1: Hi @daenney @Sean-Der @at-wat , would anybody like to run the workflows again? I think all the issues have been fixed.
epii1: @daenney thanks. But please approve again. In order to pass the git message lint, I have squashed all the commits.
epii1: hi @at-wat , do you have time to review the latest changes?
epii1: @at-wat please check again, all the issues have been fixed.
epii1: Hi @daenney @Sean-Der , as this PR has been reviewed by @at-wat , what else need I do to make it merged?

Patch:
diff --git a/AUTHORS.txt b/AUTHORS.txt
index 9155cfb..b43156d 100644
--- a/AUTHORS.txt
+++ b/AUTHORS.txt
@@ -38,3 +38,4 @@ Vadim <fffilimonov@yandex.ru>
 Vadim Filimonov <fffilimonov@yandex.ru>
 wmiao <wu.miao@viasat.com>
 ZHENK <chengzhenyang@gmail.com>
+吕海涛 <hi@taoshu.in>
diff --git a/config.go b/config.go
index d94fbd6..d750ac1 100644
--- a/config.go
+++ b/config.go
@@ -124,6 +124,9 @@ type Config struct {
 	// Use of KeyLogWriter compromises security and should only be
 	// used for debugging.
 	KeyLogWriter io.Writer
+
+	// SessionStore is the container to store session for resumption.
+	SessionStore SessionStore
 }
 
 func defaultConnectContextMaker() (context.Context, func()) {
diff --git a/conn.go b/conn.go
index fb6a4d2..958fa4b 100644
--- a/conn.go
+++ b/conn.go
@@ -26,6 +26,7 @@ import (
 const (
 	initialTickerInterval = time.Second
 	cookieLength          = 20
+	sessionLength         = 32
 	defaultNamedCurve     = elliptic.X25519
 	inboundBufferSize     = 8192
 	// Default replay protection window is specified by RFC 6347 Section 4.1.2.6
@@ -172,6 +173,7 @@ func createConn(ctx context.Context, nextConn net.Conn, config *Config, isClient
 		log:                         logger,
 		initialEpoch:                0,
 		keyLogWriter:                config.KeyLogWriter,
+		sessionStore:                config.SessionStore,
 	}
 
 	var initialFlight flightVal
@@ -754,6 +756,16 @@ func (c *Conn) recvHandshake() <-chan chan struct{} {
 }
 
 func (c *Conn) notify(ctx context.Context, level alert.Level, desc alert.Description) error {
+	if level == alert.Fatal && len(c.state.SessionID) > 0 {
+		// According to the RFC, we need to delete the stored session.
+		// https://datatracker.ietf.org/doc/html/rfc5246#section-7.2
+		if ss := c.fsm.cfg.sessionStore; ss != nil {
+			c.log.Tracef("clean invalid session: %s", c.state.SessionID)
+			if err := ss.Del(c.sessionKey()); err != nil {
+				return err
+			}
+		}
+	}
 	return c.writePackets(ctx, []*packet{
 		{
 			record: &recordlayer.RecordLayer{
@@ -950,6 +962,16 @@ func (c *Conn) RemoteAddr() net.Addr {
 	return c.nextConn.RemoteAddr()
 }
 
+func (c *Conn) sessionKey() []byte {
+	if c.state.isClient {
+		// As ServerName can be like 0.example.com, it's better to add
+		// delimiter character which is not allowed to be in
+		// neither address or domain name.
+		return []byte(c.nextConn.RemoteAddr().String() + "_" + c.fsm.cfg.serverName)
+	}
+	return c.state.SessionID
+}
+
 // SetDeadline implements net.Conn.SetDeadline
 func (c *Conn) SetDeadline(t time.Time) error {
 	c.readDeadline.Set(t)
diff --git a/flight.go b/flight.go
index 580ee48..bed3f8c 100644
--- a/flight.go
+++ b/flight.go
@@ -6,6 +6,9 @@ package dtls
   of a number of messages, they should be viewed as monolithic for the
   purpose of timeout and retransmission.
   https://tools.ietf.org/html/rfc4347#section-4.2.4
+
+  Message flights for full handshake:
+
   Client                                          Server
   ------                                          ------
                                       Waiting                 Flight 0
@@ -31,6 +34,23 @@ package dtls
                                       [ChangeCipherSpec]    \ Flight 6
                           <--------             Finished    /
 
+  Message flights for session-resuming handshake (no cookie exchange):
+
+  Client                                          Server
+  ------                                          ------
+                                      Waiting                 Flight 0
+
+  ClientHello             -------->                           Flight 1
+
+                                             ServerHello    \
+                                      [ChangeCipherSpec]      Flight 4b
+                          <--------             Finished    /
+
+  [ChangeCipherSpec]                                        \ Flight 5b
+  Finished                -------->                         /
+
+                                      [ChangeCipherSpec]    \ Flight 6
+                          <--------             Finished    /
 */
 
 type flightVal uint8
@@ -41,7 +61,9 @@ const (
 	flight2
 	flight3
 	flight4
+	flight4b
 	flight5
+	flight5b
 	flight6
 )
 
@@ -57,8 +79,12 @@ func (f flightVal) String() string {
 		return "Flight 3"
 	case flight4:
 		return "Flight 4"
+	case flight4b:
+		return "Flight 4b"
 	case flight5:
 		return "Flight 5"
+	case flight5b:
+		return "Flight 5b"
 	case flight6:
 		return "Flight 6"
 	default:
@@ -67,9 +93,9 @@ func (f flightVal) String() string {
 }
 
 func (f flightVal) isLastSendFlight() bool {
-	return f == flight6
+	return f == flight6 || f == flight5b
 }
 
 func (f flightVal) isLastRecvFlight() bool {
-	return f == flight5
+	return f == flight5 || f == flight4b
 }
diff --git a/flight0handler.go b/flight0handler.go
index 949d7c0..2e535bf 100644
--- a/flight0handler.go
+++ b/flight0handler.go
@@ -79,7 +79,30 @@ func flight0Parse(ctx context.Context, c flightConn, state *State, cache *handsh
 		}
 	}
 
-	return flight2, nil, nil
+	return handleHelloResume(clientHello.SessionID, state, cfg, flight2)
+}
+
+func handleHelloResume(sessionID []byte, state *State, cfg *handshakeConfig, next flightVal) (flightVal, *alert.Alert, error) {
+	if len(sessionID) > 0 && cfg.sessionStore != nil {
+		if s, err := cfg.sessionStore.Get(sessionID); err != nil {
+			return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
+		} else if s.ID != nil {
+			cfg.log.Tracef("[handshake] resume session: %x", sessionID)
+
+			state.SessionID = sessionID
+			state.masterSecret = s.Secret
+
+			if err := state.initCipherSuite(); err != nil {
+				return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
+			}
+
+			clientRandom := state.localRandom.MarshalFixed()
+			cfg.writeKeyLog(keyLogLabelTLS12, clientRandom[:], state.masterSecret)
+
+			return flight4b, nil, nil
+		}
+	}
+	return next, nil, nil
 }
 
 func flight0Generate(c flightConn, state *State, cache *handshakeCache, cfg *handshakeConfig) ([]*packet, *alert.Alert, error) {
diff --git a/flight1handler.go b/flight1handler.go
index 9229292..721e946 100644
--- a/flight1handler.go
+++ b/flight1handler.go
@@ -90,6 +90,18 @@ func flight1Generate(c flightConn, state *State, cache *handshakeCache, cfg *han
 		extensions = append(extensions, &extension.ServerName{ServerName: cfg.serverName})
 	}
 
+	if cfg.sessionStore != nil {
+		cfg.log.Tracef("[handshake] try to resume session")
+		if s, err := cfg.sessionStore.Get(c.sessionKey()); err != nil {
+			return nil, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
+		} else if s.ID != nil {
+			cfg.log.Tracef("[handshake] get saved session: %x", s.ID)
+
+			state.SessionID = s.ID
+			state.masterSecret = s.Secret
+		}
+	}
+
 	return []*packet{
 		{
 			record: &recordlayer.RecordLayer{
@@ -99,6 +111,7 @@ func flight1Generate(c flightConn, state *State, cache *handshakeCache, cfg *han
 				Content: &handshake.Handshake{
 					Message: &handshake.MessageClientHello{
 						Version:            protocol.Version1_2,
+						SessionID:          state.SessionID,
 						Cookie:             state.cookie,
 						Random:             state.localRandom,
 						CipherSuiteIDs:     cipherSuiteIDs(cfg.localCipherSuites),
diff --git a/flight3handler.go b/flight3handler.go
index f953be8..b152a0e 100644
--- a/flight3handler.go
+++ b/flight3handler.go
@@ -1,6 +1,7 @@
 package dtls
 
 import (
+	"bytes"
 	"context"
 
 	"github.com/pion/dtls/v2/pkg/crypto/elliptic"
@@ -32,36 +33,23 @@ func flight3Parse(ctx context.Context, c flightConn, state *State, cache *handsh
 		}
 	}
 
-	if cfg.localPSKCallback != nil {
-		seq, msgs, ok = cache.fullPullMap(state.handshakeRecvSequence,
-			handshakeCachePullRule{handshake.TypeServerHello, cfg.initialEpoch, false, false},
-			handshakeCachePullRule{handshake.TypeServerKeyExchange, cfg.initialEpoch, false, true},
-			handshakeCachePullRule{handshake.TypeServerHelloDone, cfg.initialEpoch, false, false},
-		)
-	} else {
-		seq, msgs, ok = cache.fullPullMap(state.handshakeRecvSequence,
-			handshakeCachePullRule{handshake.TypeServerHello, cfg.initialEpoch, false, false},
-			handshakeCachePullRule{handshake.TypeCertificate, cfg.initialEpoch, false, true},
-			handshakeCachePullRule{handshake.TypeServerKeyExchange, cfg.initialEpoch, false, false},
-			handshakeCachePullRule{handshake.TypeCertificateRequest, cfg.initialEpoch, false, true},
-			handshakeCachePullRule{handshake.TypeServerHelloDone, cfg.initialEpoch, false, false},
-		)
-	}
+	_, msgs, ok = cache.fullPullMap(state.handshakeRecvSequence,
+		handshakeCachePullRule{handshake.TypeServerHello, cfg.initialEpoch, false, false},
+	)
 	if !ok {
 		// Don't have enough messages. Keep reading
 		return 0, nil, nil
 	}
-	state.handshakeRecvSequence = seq
 
-	if h, ok := msgs[handshake.TypeServerHello].(*handshake.MessageServerHello); ok {
+	if h, msgOk := msgs[handshake.TypeServerHello].(*handshake.MessageServerHello); msgOk {
 		if !h.Version.Equal(protocol.Version1_2) {
 			return 0, &alert.Alert{Level: alert.Fatal, Description: alert.ProtocolVersion}, errUnsupportedProtocolVersion
 		}
 		for _, v := range h.Extensions {
 			switch e := v.(type) {
 			case *extension.UseSRTP:
-				profile, ok := findMatchingSRTPProfile(e.ProtectionProfiles, cfg.localSRTPProtectionProfiles)
-				if !ok {
+				profile, found := findMatchingSRTPProfile(e.ProtectionProfiles, cfg.localSRTPProtectionProfiles)
+				if !found {
 					return 0, &alert.Alert{Level: alert.Fatal, Description: alert.IllegalParameter}, errClientNoMatchingSRTPProfile
 				}
 				state.srtpProtectionProfile = profile
@@ -83,15 +71,53 @@ func flight3Parse(ctx context.Context, c flightConn, state *State, cache *handsh
 			return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InsufficientSecurity}, errCipherSuiteNoIntersection
 		}
 
-		selectedCipherSuite, ok := findMatchingCipherSuite([]CipherSuite{remoteCipherSuite}, cfg.localCipherSuites)
-		if !ok {
+		selectedCipherSuite, found := findMatchingCipherSuite([]CipherSuite{remoteCipherSuite}, cfg.localCipherSuites)
+		if !found {
 			return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InsufficientSecurity}, errInvalidCipherSuite
 		}
 
 		state.cipherSuite = selectedCipherSuite
 		state.remoteRandom = h.Random
 		cfg.log.Tracef("[handshake] use cipher suite: %s", selectedCipherSuite.String())
+
+		if len(h.SessionID) > 0 && bytes.Equal(state.SessionID, h.SessionID) {
+			return handleResumption(ctx, c, state, cache, cfg)
+		}
+
+		if len(state.SessionID) > 0 {
+			cfg.log.Tracef("[handshake] clean old session : %s", state.SessionID)
+			if err := cfg.sessionStore.Del(state.SessionID); err != nil {
+				return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
+			}
+		}
+
+		if cfg.sessionStore == nil {
+			state.SessionID = []byte{}
+		} else {
+			state.SessionID = h.SessionID
+		}
+
+		state.masterSecret = []byte{}
+	}
+
+	if cfg.localPSKCallback != nil {
+		seq, msgs, ok = cache.fullPullMap(state.handshakeRecvSequence+1,
+			handshakeCachePullRule{handshake.TypeServerKeyExchange, cfg.initialEpoch, false, true},
+			handshakeCachePullRule{handshake.TypeServerHelloDone, cfg.initialEpoch, false, false},
+		)
+	} else {
+		seq, msgs, ok = cache.fullPullMap(state.handshakeRecvSequence+1,
+			handshakeCachePullRule{handshake.TypeCertificate, cfg.initialEpoch, false, true},
+			handshakeCachePullRule{handshake.TypeServerKeyExchange, cfg.initialEpoch, false, false},
+			handshakeCachePullRule{handshake.TypeCertificateRequest, cfg.initialEpoch, false, true},
+			handshakeCachePullRule{handshake.TypeServerHelloDone, cfg.initialEpoch, false, false},
+		)
+	}
+	if !ok {
+		// Don't have enough messages. Keep reading
+		return 0, nil, nil
 	}
+	state.handshakeRecvSequence = seq
 
 	if h, ok := msgs[handshake.TypeCertificate].(*handshake.MessageCertificate); ok {
 		state.PeerCertificates = h.Certificate
@@ -113,6 +139,47 @@ func flight3Parse(ctx context.Context, c flightConn, state *State, cache *handsh
 	return flight5, nil, nil
 }
 
+func handleResumption(ctx context.Context, c flightConn, state *State, cache *handshakeCache, cfg *handshakeConfig) (flightVal, *alert.Alert, error) {
+	if err := state.initCipherSuite(); err != nil {
+		return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
+	}
+
+	// Now, encrypted packets can be handled
+	if err := c.handleQueuedPackets(ctx); err != nil {
+		return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
+	}
+
+	_, msgs, ok := cache.fullPullMap(state.handshakeRecvSequence+1,
+		handshakeCachePullRule{handshake.TypeFinished, cfg.initialEpoch + 1, false, false},
+	)
+	if !ok {
+		// No valid message received. Keep reading
+		return 0, nil, nil
+	}
+
+	var finished *handshake.MessageFinished
+	if finished, ok = msgs[handshake.TypeFinished].(*handshake.MessageFinished); !ok {
+		return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, nil
+	}
+	plainText := cache.pullAndMerge(
+		handshakeCachePullRule{handshake.TypeClientHello, cfg.initialEpoch, true, false},
+		handshakeCachePullRule{handshake.TypeServerHello, cfg.initialEpoch, false, false},
+	)
+
+	expectedVerifyData, err := prf.VerifyDataServer(state.masterSecret, plainText, state.cipherSuite.HashFunc())
+	if err != nil {
+		return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
+	}
+	if !bytes.Equal(expectedVerifyData, finished.VerifyData) {
+		return 0, &alert.Alert{Level: alert.Fatal, Description: alert.HandshakeFailure}, errVerifyDataMismatch
+	}
+
+	clientRandom := state.localRandom.MarshalFixed()
+	cfg.writeKeyLog(keyLogLabelTLS12, clientRandom[:], state.masterSecret)
+
+	return flight5b, nil, nil
+}
+
 func handleServerKeyExchange(_ flightConn, state *State, cfg *handshakeConfig, h *handshake.MessageServerKeyExchange) (*alert.Alert, error) {
 	var err error
 	if cfg.localPSKCallback != nil {
@@ -181,6 +248,7 @@ func flight3Generate(c flightConn, state *State, cache *handshakeCache, cfg *han
 				Content: &handshake.Handshake{
 					Message: &handshake.MessageClientHello{
 						Version:            protocol.Version1_2,
+						SessionID:          state.SessionID,
 						Cookie:             state.cookie,
 						Random:             state.localRandom,
 						CipherSuiteIDs:     cipherSuiteIDs(cfg.localCipherSuites),
diff --git a/flight4bhandler.go b/flight4bhandler.go
new file mode 100644
index 0000000..651dd23
--- /dev/null
+++ b/flight4bhandler.go
@@ -0,0 +1,130 @@
+package dtls
+
+import (
+	"bytes"
+	"context"
+
+	"github.com/pion/dtls/v2/pkg/crypto/prf"
+	"github.com/pion/dtls/v2/pkg/protocol"
+	"github.com/pion/dtls/v2/pkg/protocol/alert"
+	"github.com/pion/dtls/v2/pkg/protocol/extension"
+	"github.com/pion/dtls/v2/pkg/protocol/handshake"
+	"github.com/pion/dtls/v2/pkg/protocol/recordlayer"
+)
+
+func flight4bParse(ctx context.Context, c flightConn, state *State, cache *handshakeCache, cfg *handshakeConfig) (flightVal, *alert.Alert, error) {
+	_, msgs, ok := cache.fullPullMap(state.handshakeRecvSequence,
+		handshakeCachePullRule{handshake.TypeFinished, cfg.initialEpoch + 1, true, false},
+	)
+	if !ok {
+		// No valid message received. Keep reading
+		return 0, nil, nil
+	}
+
+	var finished *handshake.MessageFinished
+	if finished, ok = msgs[handshake.TypeFinished].(*handshake.MessageFinished); !ok {
+		return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, nil
+	}
+
+	plainText := cache.pullAndMerge(
+		handshakeCachePullRule{handshake.TypeClientHello, cfg.initialEpoch, true, false},
+		handshakeCachePullRule{handshake.TypeServerHello, cfg.initialEpoch, false, false},
+		handshakeCachePullRule{handshake.TypeFinished, cfg.initialEpoch + 1, false, false},
+	)
+
+	expectedVerifyData, err := prf.VerifyDataClient(state.masterSecret, plainText, state.cipherSuite.HashFunc())
+	if err != nil {
+		return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
+	}
+	if !bytes.Equal(expectedVerifyData, finished.VerifyData) {
+		return 0, &alert.Alert{Level: alert.Fatal, Description: alert.HandshakeFailure}, errVerifyDataMismatch
+	}
+
+	// Other party may re-transmit the last flight. Keep state to be flight4b.
+	return flight4b, nil, nil
+}
+
+func flight4bGenerate(c flightConn, state *State, cache *handshakeCache, cfg *handshakeConfig) ([]*packet, *alert.Alert, error) {
+	var pkts []*packet
+
+	extensions := []extension.Extension{&extension.RenegotiationInfo{
+		RenegotiatedConnection: 0,
+	}}
+	if (cfg.extendedMasterSecret == RequestExtendedMasterSecret ||
+		cfg.extendedMasterSecret == RequireExtendedMasterSecret) && state.extendedMasterSecret {
+		extensions = append(extensions, &extension.UseExtendedMasterSecret{
+			Supported: true,
+		})
+	}
+	if state.srtpProtectionProfile != 0 {
+		extensions = append(extensions, &extension.UseSRTP{
+			ProtectionProfiles: []SRTPProtectionProfile{state.srtpProtectionProfile},
+		})
+	}
+
+	cipherSuiteID := uint16(state.cipherSuite.ID())
+	serverHello := &handshake.Handshake{
+		Message: &handshake.MessageServerHello{
+			Version:           protocol.Version1_2,
+			Random:            state.localRandom,
+			SessionID:         state.SessionID,
+			CipherSuiteID:     &cipherSuiteID,
+			CompressionMethod: defaultCompressionMethods()[0],
+			Extensions:        extensions,
+		},
+	}
+
+	serverHello.Header.MessageSequence = uint16(state.handshakeSendSequence)
+
+	if len(state.localVerifyData) == 0 {
+		plainText := cache.pullAndMerge(
+			handshakeCachePullRule{handshake.TypeClientHello, cfg.initialEpoch, true, false},
+		)
+		raw, err := serverHello.Marshal()
+		if err != nil {
+			return nil, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
+		}
+		plainText = append(plainText, raw...)
+
+		state.localVerifyData, err = prf.VerifyDataServer(state.masterSecret, plainText, state.cipherSuite.HashFunc())
+		if err != nil {
+			return nil, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
+		}
+	}
+
+	pkts = append(pkts,
+		&packet{
+			record: &recordlayer.RecordLayer{
+				Header: recordlayer.Header{
+					Version: protocol.Version1_2,
+				},
+				Content: serverHello,
+			},
+		},
+		&packet{
+			record: &recordlayer.RecordLayer{
+				Header: recordlayer.Header{
+					Version: protocol.Version1_2,
+				},
+				Content: &protocol.ChangeCipherSpec{},
+			},
+		},
+		&packet{
+			record: &recordlayer.RecordLayer{
+				Header: recordlayer.Header{
+					Version: protocol.Version1_2,
+					Epoch:   1,
+				},
+				Content: &handshake.Handshake{
+					Message: &handshake.MessageFinished{
+						VerifyData: state.localVerifyData,
+					},
+				},
+			},
+			shouldEncrypt:            true,
+			resetLocalSequenceNumber: true,
+		},
+	)
+
+	return pkts, nil, nil
+}
diff --git a/flight4handler.go b/flight4handler.go
index 9cba643..3a72c13 100644
--- a/flight4handler.go
+++ b/flight4handler.go
@@ -2,6 +2,7 @@ package dtls
 
 import (
 	"context"
+	"crypto/rand"
 	"crypto/x509"
 
 	"github.com/pion/dtls/v2/pkg/crypto/clientcertificate"
@@ -34,6 +35,12 @@ func flight4Parse(ctx context.Context, c flightConn, state *State, cache *handsh
 
 	if h, hasCert := msgs[handshake.TypeCertificate].(*handshake.MessageCertificate); hasCert {
 		state.PeerCertificates = h.Certificate
+		// If the client offer its certificate, just disable session resumption.
+		// Otherwise, we have to store the certificate identitfication and expire time.
+		// And we have to check whether this certificate expired, revoked or changed.
+		//
+		// https://curl.se/docs/CVE-2016-5419.html
+		state.SessionID = nil
 	}
 
 	if h, hasCertVerify := msgs[handshake.TypeCertificateVerify].(*handshake.MessageCertificateVerify); hasCertVerify {
@@ -128,6 +135,17 @@ func flight4Parse(ctx context.Context, c flightConn, state *State, cache *handsh
 		cfg.writeKeyLog(keyLogLabelTLS12, clientRandom[:], state.masterSecret)
 	}
 
+	if len(state.SessionID) > 0 {
+		s := Session{
+			ID:     state.SessionID,
+			Secret: state.masterSecret,
+		}
+		cfg.log.Tracef("[handshake] save new session: %x", s.ID)
+		if err := cfg.sessionStore.Set(state.SessionID, s); err != nil {
+			return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
+		}
+	}
+
 	// Now, encrypted packets can be handled
 	if err := c.handleQueuedPackets(ctx); err != nil {
 		return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
@@ -197,6 +215,13 @@ func flight4Generate(c flightConn, state *State, cache *handshakeCache, cfg *han
 	var pkts []*packet
 	cipherSuiteID := uint16(state.cipherSuite.ID())
 
+	if cfg.sessionStore != nil {
+		state.SessionID = make([]byte, sessionLength)
+		if _, err := rand.Read(state.SessionID); err != nil {
+			return nil, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
+		}
+	}
+
 	pkts = append(pkts, &packet{
 		record: &recordlayer.RecordLayer{
 			Header: recordlayer.Header{
@@ -206,6 +231,7 @@ func flight4Generate(c flightConn, state *State, cache *handshakeCache, cfg *han
 				Message: &handshake.MessageServerHello{
 					Version:           protocol.Version1_2,
 					Random:            state.localRandom,
+					SessionID:         state.SessionID,
 					CipherSuiteID:     &cipherSuiteID,
 					CompressionMethod: defaultCompressionMethods()[0],
 					Extensions:        extensions,
diff --git a/flight5bhandler.go b/flight5bhandler.go
new file mode 100644
index 0000000..577342e
--- /dev/null
+++ b/flight5bhandler.go
@@ -0,0 +1,75 @@
+package dtls
+
+import (
+	"context"
+
+	"github.com/pion/dtls/v2/pkg/crypto/prf"
+	"github.com/pion/dtls/v2/pkg/protocol"
+	"github.com/pion/dtls/v2/pkg/protocol/alert"
+	"github.com/pion/dtls/v2/pkg/protocol/handshake"
+	"github.com/pion/dtls/v2/pkg/protocol/recordlayer"
+)
+
+func flight5bParse(ctx context.Context, c flightConn, state *State, cache *handshakeCache, cfg *handshakeConfig) (flightVal, *alert.Alert, error) {
+	_, msgs, ok := cache.fullPullMap(state.handshakeRecvSequence-1,
+		handshakeCachePullRule{handshake.TypeFinished, cfg.initialEpoch + 1, false, false},
+	)
+	if !ok {
+		// No valid message received. Keep reading
+		return 0, nil, nil
+	}
+
+	if _, ok = msgs[handshake.TypeFinished].(*handshake.MessageFinished); !ok {
+		return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, nil
+	}
+
+	// Other party may re-transmit the last flight. Keep state to be flight5b.
+	return flight5b, nil, nil
+}
+
+func flight5bGenerate(c flightConn, state *State, cache *handshakeCache, cfg *handshakeConfig) ([]*packet, *alert.Alert, error) { //nolint:gocognit
+	var pkts []*packet
+
+	pkts = append(pkts,
+		&packet{
+			record: &recordlayer.RecordLayer{
+				Header: recordlayer.Header{
+					Version: protocol.Version1_2,
+				},
+				Content: &protocol.ChangeCipherSpec{},
+			},
+		})
+
+	if len(state.localVerifyData) == 0 {
+		plainText := cache.pullAndMerge(
+			handshakeCachePullRule{handshake.TypeClientHello, cfg.initialEpoch, true, false},
+			handshakeCachePullRule{handshake.TypeServerHello, cfg.initialEpoch, false, false},
+			handshakeCachePullRule{handshake.TypeFinished, cfg.initialEpoch + 1, false, false},
+		)
+
+		var err error
+		state.localVerifyData, err = prf.VerifyDataClient(state.masterSecret, plainText, state.cipherSuite.HashFunc())
+		if err != nil {
+			return nil, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
+		}
+	}
+
+	pkts = append(pkts,
+		&packet{
+			record: &recordlayer.RecordLayer{
+				Header: recordlayer.Header{
+					Version: protocol.Version1_2,
+					Epoch:   1,
+				},
+				Content: &handshake.Handshake{
+					Message: &handshake.MessageFinished{
+						VerifyData: state.localVerifyData,
+					},
+				},
+			},
+			shouldEncrypt:            true,
+			resetLocalSequenceNumber: true,
+		})
+
+	return pkts, nil, nil
+}
diff --git a/flight5handler.go b/flight5handler.go
index baa1d5c..510fa45 100644
--- a/flight5handler.go
+++ b/flight5handler.go
@@ -48,6 +48,17 @@ func flight5Parse(ctx context.Context, c flightConn, state *State, cache *handsh
 		return 0, &alert.Alert{Level: alert.Fatal, Description: alert.HandshakeFailure}, errVerifyDataMismatch
 	}
 
+	if len(state.SessionID) > 0 {
+		s := Session{
+			ID:     state.SessionID,
+			Secret: state.masterSecret,
+		}
+		cfg.log.Tracef("[handshake] save new session: %x", s.ID)
+		if err := cfg.sessionStore.Set(c.sessionKey(), s); err != nil {
+			return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
+		}
+	}
+
 	return flight5, nil, nil
 }
 
diff --git a/flight6handler.go b/flight6handler.go
index 10de5ad..fddaa0e 100644
--- a/flight6handler.go
+++ b/flight6handler.go
@@ -23,7 +23,7 @@ func flight6Parse(ctx context.Context, c flightConn, state *State, cache *handsh
 		return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, nil
 	}
 
-	// Other party retransmitted the last flight.
+	// Other party may re-transmit the last flight. Keep state to be flight6.
 	return flight6, nil, nil
 }
 
diff --git a/flighthandler.go b/flighthandler.go
index b364c09..f899ffa 100644
--- a/flighthandler.go
+++ b/flighthandler.go
@@ -24,8 +24,12 @@ func (f flightVal) getFlightParser() (flightParser, error) {
 		return flight3Parse, nil
 	case flight4:
 		return flight4Parse, nil
+	case flight4b:
+		return flight4bParse, nil
 	case flight5:
 		return flight5Parse, nil
+	case flight5b:
+		return flight5bParse, nil
 	case flight6:
 		return flight6Parse, nil
 	default:
@@ -47,8 +51,12 @@ func (f flightVal) getFlightGenerator() (gen flightGenerator, retransmit bool, e
 		return flight3Generate, true, nil
 	case flight4:
 		return flight4Generate, true, nil
+	case flight4b:
+		return flight4bGenerate, true, nil
 	case flight5:
 		return flight5Generate, true, nil
+	case flight5b:
+		return flight5bGenerate, true, nil
 	case flight6:
 		return flight6Generate, true, nil
 	default:
diff --git a/go.mod b/go.mod
index f0cf52d..9cff776 100644
--- a/go.mod
+++ b/go.mod
@@ -4,8 +4,8 @@ require (
 	github.com/pion/logging v0.2.2
 	github.com/pion/transport v0.13.0
 	github.com/pion/udp v0.1.1
-	golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871
-	golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c
+	golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3
+	golang.org/x/net v0.0.0-20211216030914-fe4d6282115f
 	golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1
 )
 
diff --git a/go.sum b/go.sum
index d8e34c9..440aa01 100644
--- a/go.sum
+++ b/go.sum
@@ -15,13 +15,14 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 h1:/pEO3GD/ABYAjuakUS6xSEmmlyVS4kxBNkeA9tLJiTI=
-golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M=
+golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c h1:WtYZ93XtWSO5KlOMgPZu7hXY9WhMZpprvlm5VwvAl8c=
 golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20211216030914-fe4d6282115f h1:hEYJvxw1lSnWIl8X9ofsYMklzaDs90JI2az5YMd4fPM=
+golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
diff --git a/handshaker.go b/handshaker.go
index 0f5077e..cefc8f6 100644
--- a/handshaker.go
+++ b/handshaker.go
@@ -100,6 +100,7 @@ type handshakeConfig struct {
 	nameToCertificate           map[string]*tls.Certificate
 	insecureSkipVerify          bool
 	verifyPeerCertificate       func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error
+	sessionStore                SessionStore
 	rootCAs                     *x509.CertPool
 	clientCAs                   *x509.CertPool
 	retransmitInterval          time.Duration
@@ -120,6 +121,7 @@ type flightConn interface {
 	recvHandshake() <-chan chan struct{}
 	setLocalEpoch(epoch uint16)
 	handleQueuedPackets(context.Context) error
+	sessionKey() []byte
 }
 
 func (c *handshakeConfig) writeKeyLog(label string, clientRandom, secret []byte) {
diff --git a/pkg/protocol/handshake/message_client_hello.go b/pkg/protocol/handshake/message_client_hello.go
index cb8a100..1deca38 100644
--- a/pkg/protocol/handshake/message_client_hello.go
+++ b/pkg/protocol/handshake/message_client_hello.go
@@ -19,6 +19,8 @@ type MessageClientHello struct {
 	Random  Random
 	Cookie  []byte
 
+	SessionID []byte
+
 	CipherSuiteIDs     []uint16
 	CompressionMethods []*protocol.CompressionMethod
 	Extensions         []extension.Extension
@@ -44,7 +46,8 @@ func (m *MessageClientHello) Marshal() ([]byte, error) {
 	rand := m.Random.MarshalFixed()
 	copy(out[2:], rand[:])
 
-	out = append(out, 0x00) // SessionID
+	out = append(out, byte(len(m.SessionID)))
+	out = append(out, m.SessionID...)
 
 	out = append(out, byte(len(m.Cookie)))
 	out = append(out, m.Cookie...)
@@ -74,7 +77,6 @@ func (m *MessageClientHello) Unmarshal(data []byte) error {
 
 	// rest of packet has variable width sections
 	currOffset := handshakeMessageClientHelloVariableWidthStart
-	currOffset += int(data[currOffset]) + 1 // SessionID
 
 	currOffset++
 	if len(data) <= currOffset {
@@ -84,6 +86,17 @@ func (m *MessageClientHello) Unmarshal(data []byte) error {
 	if len(data) <= currOffset+n {
 		return errBufferTooSmall
 	}
+	m.SessionID = append([]byte{}, data[currOffset:currOffset+n]...)
+	currOffset += len(m.SessionID)
+
+	currOffset++
+	if len(data) <= currOffset {
+		return errBufferTooSmall
+	}
+	n = int(data[currOffset-1])
+	if len(data) <= currOffset+n {
+		return errBufferTooSmall
+	}
 	m.Cookie = append([]byte{}, data[currOffset:currOffset+n]...)
 	currOffset += len(m.Cookie)
 
diff --git a/pkg/protocol/handshake/message_server_hello.go b/pkg/protocol/handshake/message_server_hello.go
index c4b181f..9c1cc22 100644
--- a/pkg/protocol/handshake/message_server_hello.go
+++ b/pkg/protocol/handshake/message_server_hello.go
@@ -17,6 +17,8 @@ type MessageServerHello struct {
 	Version protocol.Version
 	Random  Random
 
+	SessionID []byte
+
 	CipherSuiteID     *uint16
 	CompressionMethod *protocol.CompressionMethod
 	Extensions        []extension.Extension
@@ -44,7 +46,8 @@ func (m *MessageServerHello) Marshal() ([]byte, error) {
 	rand := m.Random.MarshalFixed()
 	copy(out[2:], rand[:])
 
-	out = append(out, 0x00) // SessionID
+	out = append(out, byte(len(m.SessionID)))
+	out = append(out, m.SessionID...)
 
 	out = append(out, []byte{0x00, 0x00}...)
 	binary.BigEndian.PutUint16(out[len(out)-2:], *m.CipherSuiteID)
@@ -73,10 +76,17 @@ func (m *MessageServerHello) Unmarshal(data []byte) error {
 	m.Random.UnmarshalFixed(random)
 
 	currOffset := messageServerHelloVariableWidthStart
-	currOffset += int(data[currOffset]) + 1 // SessionID
-	if len(data) < (currOffset + 2) {
+	currOffset++
+	if len(data) <= currOffset {
+		return errBufferTooSmall
+	}
+
+	n := int(data[currOffset-1])
+	if len(data) <= currOffset+n {
 		return errBufferTooSmall
 	}
+	m.SessionID = append([]byte{}, data[currOffset:currOffset+n]...)
+	currOffset += len(m.SessionID)
 
 	m.CipherSuiteID = new(uint16)
 	*m.CipherSuiteID = binary.BigEndian.Uint16(data[currOffset:])
diff --git a/session.go b/session.go
new file mode 100644
index 0000000..f52120c
--- /dev/null
+++ b/session.go
@@ -0,0 +1,21 @@
+package dtls
+
+// Session store data needed in resumption
+type Session struct {
+	// ID store session id
+	ID []byte
+	// Secret store session master secret
+	Secret []byte
+}
+
+// SessionStore defines methods needed for session resumption.
+type SessionStore interface {
+	// Set save a session.
+	// For client, use server name as key.
+	// For server, use session id.
+	Set(key []byte, s Session) error
+	// Get fetch a session.
+	Get(key []byte) (Session, error)
+	// Del clean saved session.
+	Del(key []byte) error
+}
diff --git a/state.go b/state.go
index 53fca6a..905c4f1 100644
--- a/state.go
+++ b/state.go
@@ -22,6 +22,7 @@ type State struct {
 	srtpProtectionProfile SRTPProtectionProfile // Negotiated SRTPProtectionProfile
 	PeerCertificates      [][]byte
 	IdentityHint          []byte
+	SessionID             []byte
 
 	isClient bool
 
@@ -54,6 +55,7 @@ type serializedState struct {
 	SRTPProtectionProfile uint16
 	PeerCertificates      [][]byte
 	IdentityHint          []byte
+	SessionID             []byte
 	IsClient              bool
 }
 
@@ -82,6 +84,7 @@ func (s *State) serialize() *serializedState {
 		SRTPProtectionProfile: uint16(s.srtpProtectionProfile),
 		PeerCertificates:      s.PeerCertificates,
 		IdentityHint:          s.IdentityHint,
+		SessionID:             s.SessionID,
 		IsClient:              s.isClient,
 	}
 }
@@ -119,6 +122,7 @@ func (s *State) deserialize(serialized serializedState) {
 	// Set remote certificate
 	s.PeerCertificates = serialized.PeerCertificates
 	s.IdentityHint = serialized.IdentityHint
+	s.SessionID = serialized.SessionID
 }
 
 func (s *State) initCipherSuite() error {


Test patch:
diff --git a/conn_test.go b/conn_test.go
index 06a802f..46cbf49 100644
--- a/conn_test.go
+++ b/conn_test.go
@@ -6,6 +6,7 @@ import (
 	"crypto/rand"
 	"crypto/tls"
 	"crypto/x509"
+	"encoding/hex"
 	"errors"
 	"fmt"
 	"io"
@@ -1584,11 +1585,6 @@ func TestProtocolVersionValidation(t *testing.T) {
 	var rand [28]byte
 	random := handshake.Random{GMTUnixTime: time.Unix(500, 0), RandomBytes: rand}
 
-	localKeypair, err := elliptic.GenerateKeypair(elliptic.X25519)
-	if err != nil {
-		t.Fatal(err)
-	}
-
 	config := &Config{
 		CipherSuites:   []CipherSuiteID{TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256},
 		FlightInterval: 100 * time.Millisecond,
@@ -1740,49 +1736,6 @@ func TestProtocolVersionValidation(t *testing.T) {
 							},
 						},
 					},
-					{
-						Header: recordlayer.Header{
-							Version:        protocol.Version1_2,
-							SequenceNumber: 2,
-						},
-						Content: &handshake.Handshake{
-							Header: handshake.Header{
-								MessageSequence: 2,
-							},
-							Message: &handshake.MessageCertificate{},
-						},
-					},
-					{
-						Header: recordlayer.Header{
-							Version:        protocol.Version1_2,
-							SequenceNumber: 3,
-						},
-						Content: &handshake.Handshake{
-							Header: handshake.Header{
-								MessageSequence: 3,
-							},
-							Message: &handshake.MessageServerKeyExchange{
-								EllipticCurveType:  elliptic.CurveTypeNamedCurve,
-								NamedCurve:         elliptic.X25519,
-								PublicKey:          localKeypair.PublicKey,
-								HashAlgorithm:      hash.SHA256,
-								SignatureAlgorithm: signature.ECDSA,
-								Signature:          make([]byte, 64),
-							},
-						},
-					},
-					{
-						Header: recordlayer.Header{
-							Version:        protocol.Version1_2,
-							SequenceNumber: 4,
-						},
-						Content: &handshake.Handshake{
-							Header: handshake.Header{
-								MessageSequence: 4,
-							},
-							Message: &handshake.MessageServerHelloDone{},
-						},
-					},
 				},
 			},
 		}
@@ -2190,3 +2143,159 @@ func TestSupportedGroupsExtension(t *testing.T) {
 		}
 	})
 }
+
+func TestSessionResume(t *testing.T) {
+	// Limit runtime in case of deadlocks
+	lim := test.TimeOut(time.Second * 20)
+	defer lim.Stop()
+
+	// Check for leaking routines
+	report := test.CheckRoutines(t)
+	defer report()
+
+	t.Run("resumed", func(t *testing.T) {
+		ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+		defer cancel()
+
+		type result struct {
+			c   *Conn
+			err error
+		}
+		clientRes := make(chan result, 1)
+
+		ss := &memSessStore{}
+
+		id, _ := hex.DecodeString("9b9fc92255634d9fb109febed42166717bb8ded8c738ba71bc7f2a0d9dae0306")
+		secret, _ := hex.DecodeString("2e942a37aca5241deb2295b5fcedac221c7078d2503d2b62aeb48c880d7da73c001238b708559686b9da6e829c05ead7")
+
+		s := Session{ID: id, Secret: secret}
+
+		ca, cb := dpipe.Pipe()
+
+		_ = ss.Set(id, s)
+		_ = ss.Set([]byte(ca.RemoteAddr().String()+"_example.com"), s)
+
+		go func() {
+			config := &Config{
+				CipherSuites: []CipherSuiteID{TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256},
+				ServerName:   "example.com",
+				SessionStore: ss,
+				MTU:          100,
+			}
+			c, err := testClient(ctx, ca, config, false)
+			clientRes <- result{c, err}
+		}()
+
+		config := &Config{
+			CipherSuites: []CipherSuiteID{TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256},
+			ServerName:   "example.com",
+			SessionStore: ss,
+			MTU:          100,
+		}
+		server, err := testServer(ctx, cb, config, true)
+		if err != nil {
+			t.Fatalf("TestSessionResume: Server failed(%v)", err)
+		}
+
+		actualSessionID := server.ConnectionState().SessionID
+		actualMasterSecret := server.ConnectionState().masterSecret
+		if !bytes.Equal(actualSessionID, id) {
+			t.Errorf("TestSessionResumetion: SessionID Mismatch: expected(%v) actual(%v)", id, actualSessionID)
+		}
+		if !bytes.Equal(actualMasterSecret, secret) {
+			t.Errorf("TestSessionResumetion: masterSecret Mismatch: expected(%v) actual(%v)", secret, actualMasterSecret)
+		}
+
+		defer func() {
+			_ = server.Close()
+		}()
+
+		res := <-clientRes
+		if res.err != nil {
+			t.Fatal(res.err)
+		}
+		_ = res.c.Close()
+	})
+
+	t.Run("new session", func(t *testing.T) {
+		ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+		defer cancel()
+
+		type result struct {
+			c   *Conn
+			err error
+		}
+		clientRes := make(chan result, 1)
+
+		s1 := &memSessStore{}
+		s2 := &memSessStore{}
+
+		ca, cb := dpipe.Pipe()
+		go func() {
+			config := &Config{
+				ServerName:   "example.com",
+				SessionStore: s1,
+			}
+			c, err := testClient(ctx, ca, config, false)
+			clientRes <- result{c, err}
+		}()
+
+		config := &Config{
+			SessionStore: s2,
+		}
+		server, err := testServer(ctx, cb, config, true)
+		if err != nil {
+			t.Fatalf("TestSessionResumetion: Server failed(%v)", err)
+		}
+
+		actualSessionID := server.ConnectionState().SessionID
+		actualMasterSecret := server.ConnectionState().masterSecret
+		ss, _ := s2.Get(actualSessionID)
+		if !bytes.Equal(actualMasterSecret, ss.Secret) {
+			t.Errorf("TestSessionResumetion: masterSecret Mismatch: expected(%v) actual(%v)", ss.Secret, actualMasterSecret)
+		}
+
+		defer func() {
+			_ = server.Close()
+		}()
+
+		res := <-clientRes
+		if res.err != nil {
+			t.Fatal(res.err)
+		}
+		cs, _ := s1.Get([]byte(ca.RemoteAddr().String() + "_example.com"))
+		if !bytes.Equal(actualMasterSecret, cs.Secret) {
+			t.Errorf("TestSessionResumetion: masterSecret Mismatch: expected(%v) actual(%v)", ss.Secret, actualMasterSecret)
+		}
+		_ = res.c.Close()
+	})
+}
+
+type memSessStore struct {
+	sync.Map
+}
+
+func (ms *memSessStore) Set(key []byte, s Session) error {
+	k := hex.EncodeToString(key)
+	ms.Store(k, s)
+
+	return nil
+}
+
+func (ms *memSessStore) Get(key []byte) (Session, error) {
+	k := hex.EncodeToString(key)
+
+	v, ok := ms.Load(k)
+	if !ok {
+		return Session{}, nil
+	}
+
+	return v.(Session), nil
+}
+
+func (ms *memSessStore) Del(key []byte) error {
+	k := hex.EncodeToString(key)
+	ms.Delete(k)
+
+	return nil
+}
diff --git a/handshake_test.go b/handshake_test.go
index 2174d21..64e1108 100644
--- a/handshake_test.go
+++ b/handshake_test.go
@@ -29,6 +29,7 @@ func TestHandshakeMessage(t *testing.T) {
 				GMTUnixTime: time.Unix(3056586332, 0),
 				RandomBytes: [28]byte{0x42, 0x54, 0xff, 0x86, 0xe1, 0x24, 0x41, 0x91, 0x42, 0x62, 0x15, 0xad, 0x16, 0xc9, 0x15, 0x8d, 0x95, 0x71, 0x8a, 0xbb, 0x22, 0xd7, 0x47, 0xec, 0xd8, 0x3d, 0xdc, 0x4b},
 			},
+			SessionID:          []byte{},
 			Cookie:             []byte{},
 			CipherSuiteIDs:     []uint16{},
 			CompressionMethods: []*protocol.CompressionMethod{},
diff --git a/handshaker_test.go b/handshaker_test.go
index d26b987..382b27b 100644
--- a/handshaker_test.go
+++ b/handshaker_test.go
@@ -275,3 +275,7 @@ func (c *flightTestConn) writePackets(ctx context.Context, pkts []*packet) error
 func (c *flightTestConn) handleQueuedPackets(ctx context.Context) error {
 	return nil
 }
+
+func (c *flightTestConn) sessionKey() []byte {
+	return nil
+}
diff --git a/pkg/protocol/handshake/message_client_hello_test.go b/pkg/protocol/handshake/message_client_hello_test.go
index bf75287..84d2e88 100644
--- a/pkg/protocol/handshake/message_client_hello_test.go
+++ b/pkg/protocol/handshake/message_client_hello_test.go
@@ -1,6 +1,7 @@
 package handshake
 
 import (
+	"bytes"
 	"reflect"
 	"testing"
 	"time"
@@ -24,7 +25,8 @@ func TestHandshakeMessageClientHello(t *testing.T) {
 			GMTUnixTime: time.Unix(3056586332, 0),
 			RandomBytes: [28]byte{0x42, 0x54, 0xff, 0x86, 0xe1, 0x24, 0x41, 0x91, 0x42, 0x62, 0x15, 0xad, 0x16, 0xc9, 0x15, 0x8d, 0x95, 0x71, 0x8a, 0xbb, 0x22, 0xd7, 0x47, 0xec, 0xd8, 0x3d, 0xdc, 0x4b},
 		},
-		Cookie: []byte{0xe6, 0x14, 0x3a, 0x1b, 0x04, 0xea, 0x9e, 0x7a, 0x14, 0xd6, 0x6c, 0x57, 0xd0, 0x0e, 0x32, 0x85, 0x76, 0x18, 0xde, 0xd8},
+		SessionID: []byte{},
+		Cookie:    []byte{0xe6, 0x14, 0x3a, 0x1b, 0x04, 0xea, 0x9e, 0x7a, 0x14, 0xd6, 0x6c, 0x57, 0xd0, 0x0e, 0x32, 0x85, 0x76, 0x18, 0xde, 0xd8},
 		CipherSuiteIDs: []uint16{
 			0xc02b,
 			0xc00a,
@@ -51,3 +53,36 @@ func TestHandshakeMessageClientHello(t *testing.T) {
 		t.Errorf("handshakeMessageClientHello marshal: got %#v, want %#v", raw, rawClientHello)
 	}
 }
+
+func TestHandshakeMessageClientHelloSessionID(t *testing.T) {
+	rawClientHello := []byte{
+		0xfe, 0xfd, 0xb6, 0x2f, 0xce, 0x5c, 0x42, 0x54, 0xff, 0x86, 0xe1, 0x24, 0x41, 0x91, 0x42,
+		0x62, 0x15, 0xad, 0x16, 0xc9, 0x15, 0x8d, 0x95, 0x71, 0x8a, 0xbb, 0x22, 0xd7, 0x47, 0xec,
+		0xd8, 0x3d, 0xdc, 0x4b, 0x20, 0xe0, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9,
+		0xea, 0xeb, 0xec, 0xed, 0xee, 0xef, 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8,
+		0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff, 0x14, 0xe6, 0x14, 0x3a, 0x1b, 0x04, 0xea, 0x9e,
+		0x7a, 0x14, 0xd6, 0x6c, 0x57, 0xd0, 0x0e, 0x32, 0x85, 0x76, 0x18, 0xde, 0xd8, 0x00, 0x04,
+		0xc0, 0x2b, 0xc0, 0x0a, 0x01, 0x00, 0x00, 0x08, 0x00, 0x0a, 0x00, 0x04, 0x00, 0x02, 0x00,
+		0x1d,
+	}
+
+	sessionID := []byte{
+		0xe0, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xeb, 0xec, 0xed, 0xee,
+		0xef, 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd,
+		0xfe, 0xff,
+	}
+
+	c := &MessageClientHello{}
+	if err := c.Unmarshal(rawClientHello); err != nil {
+		t.Error(err)
+	} else if !bytes.Equal(c.SessionID, sessionID) {
+		t.Errorf("handshakeMessageClientHello invalid SessionID: got %#v, want %#v", c.SessionID, sessionID)
+	}
+
+	raw, err := c.Marshal()
+	if err != nil {
+		t.Error(err)
+	} else if !reflect.DeepEqual(raw, rawClientHello) {
+		t.Errorf("handshakeMessageClientHello marshal: got %#v, want %#v", raw, rawClientHello)
+	}
+}
diff --git a/pkg/protocol/handshake/message_server_hello_test.go b/pkg/protocol/handshake/message_server_hello_test.go
index 99e7f05..5db979c 100644
--- a/pkg/protocol/handshake/message_server_hello_test.go
+++ b/pkg/protocol/handshake/message_server_hello_test.go
@@ -1,6 +1,7 @@
 package handshake
 
 import (
+	"bytes"
 	"reflect"
 	"testing"
 	"time"
@@ -25,6 +26,7 @@ func TestHandshakeMessageServerHello(t *testing.T) {
 			GMTUnixTime: time.Unix(560149025, 0),
 			RandomBytes: [28]byte{0x81, 0x0e, 0x98, 0x6c, 0x85, 0x3d, 0xa4, 0x39, 0xaf, 0x5f, 0xd6, 0x5c, 0xcc, 0x20, 0x7f, 0x7c, 0x78, 0xf1, 0x5f, 0x7e, 0x1c, 0xb7, 0xa1, 0x1e, 0xcf, 0x63, 0x84, 0x28},
 		},
+		SessionID:         []byte{},
 		CipherSuiteID:     &cipherSuiteID,
 		CompressionMethod: &protocol.CompressionMethod{},
 		Extensions:        []extension.Extension{},
@@ -44,3 +46,37 @@ func TestHandshakeMessageServerHello(t *testing.T) {
 		t.Errorf("handshakeMessageServerHello marshal: got %#v, want %#v", raw, rawServerHello)
 	}
 }
+
+func TestHandshakeMessageServerHelloSessionID(t *testing.T) {
+	rawServerHello := []byte{
+		0xfe, 0xfd, 0x21, 0x63, 0x32, 0x21, 0x81, 0x0e, 0x98, 0x6c,
+		0x85, 0x3d, 0xa4, 0x39, 0xaf, 0x5f, 0xd6, 0x5c, 0xcc, 0x20,
+		0x7f, 0x7c, 0x78, 0xf1, 0x5f, 0x7e, 0x1c, 0xb7, 0xa1, 0x1e,
+		0xcf, 0x63, 0x84, 0x28, 0x20, 0xe0, 0xe1, 0xe2, 0xe3, 0xe4,
+		0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xeb, 0xec, 0xed, 0xee,
+		0xef, 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8,
+		0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff, 0xc0, 0x2b, 0x00,
+		0x00, 0x00,
+	}
+
+	sessionID := []byte{
+		0xe0, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9,
+		0xea, 0xeb, 0xec, 0xed, 0xee, 0xef, 0xf0, 0xf1, 0xf2, 0xf3,
+		0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd,
+		0xfe, 0xff,
+	}
+
+	c := &MessageServerHello{}
+	if err := c.Unmarshal(rawServerHello); err != nil {
+		t.Error(err)
+	} else if !bytes.Equal(c.SessionID, sessionID) {
+		t.Errorf("handshakeMessageServerHello invalid SessionID: got %#v, want %#v", c.SessionID, sessionID)
+	}
+
+	raw, err := c.Marshal()
+	if err != nil {
+		t.Error(err)
+	} else if !reflect.DeepEqual(raw, rawServerHello) {
+		t.Errorf("handshakeMessageServerHello marshal: got %#v, want %#v", raw, rawServerHello)
+	}
+}