Repository: libp2p/go-libp2p
Pull request #1728 (2022-09-02 08:51:42)

noise: implement an API to send and receive early data
This is a new version of #1645, extending the API introduced in #1663. Closes #1645.
Part of #1717.

This PR is a prerequisite for moving go-libp2p-webtransport into this repo.


Patch:
diff --git a/config/config.go b/config/config.go
index 90fa9c16..4ccbc509 100644
--- a/config/config.go
+++ b/config/config.go
@@ -64,11 +64,6 @@ type Config struct {
 	// Set it via the UserAgent option function.
 	UserAgent string
 
-	// ProtocolVersion is the protocol version that identifies the family
-	// of protocols used by the peer in the Identify protocol. It is set
-	// using the [ProtocolVersion] option.
-	ProtocolVersion string
-
 	PeerKey crypto.PrivKey
 
 	Transports         []TptC
@@ -228,7 +223,6 @@ func (cfg *Config) NewNode() (host.Host, error) {
 		NATManager:          cfg.NATManager,
 		EnablePing:          !cfg.DisablePing,
 		UserAgent:           cfg.UserAgent,
-		ProtocolVersion:     cfg.ProtocolVersion,
 		MultiaddrResolver:   cfg.MultiaddrResolver,
 		EnableHolePunching:  cfg.EnableHolePunching,
 		HolePunchingOptions: cfg.HolePunchingOptions,
diff --git a/options.go b/options.go
index 3509c61d..6ed6a94d 100644
--- a/options.go
+++ b/options.go
@@ -414,15 +414,6 @@ var NoTransports = func(cfg *Config) error {
 	return nil
 }
 
-// ProtocolVersion sets the protocolVersion string required by the
-// libp2p Identify protocol.
-func ProtocolVersion(s string) Option {
-	return func(cfg *Config) error {
-		cfg.ProtocolVersion = s
-		return nil
-	}
-}
-
 // UserAgent sets the libp2p user-agent sent along with the identify protocol
 func UserAgent(userAgent string) Option {
 	return func(cfg *Config) error {
diff --git a/p2p/host/basic/basic_host.go b/p2p/host/basic/basic_host.go
index 99978763..d3642bd8 100644
--- a/p2p/host/basic/basic_host.go
+++ b/p2p/host/basic/basic_host.go
@@ -145,9 +145,6 @@ type HostOpts struct {
 	// UserAgent sets the user-agent for the host.
 	UserAgent string
 
-	// ProtocolVersion sets the protocol version for the host.
-	ProtocolVersion string
-
 	// DisableSignedPeerRecord disables the generation of Signed Peer Records on this host.
 	DisableSignedPeerRecord bool
 
@@ -229,18 +226,9 @@ func NewHost(n network.Network, opts *HostOpts) (*BasicHost, error) {
 
 	// we can't set this as a default above because it depends on the *BasicHost.
 	if h.disableSignedPeerRecord {
-		h.ids, err = identify.NewIDService(
-			h,
-			identify.UserAgent(opts.UserAgent),
-			identify.ProtocolVersion(opts.ProtocolVersion),
-			identify.DisableSignedPeerRecord(),
-		)
+		h.ids, err = identify.NewIDService(h, identify.UserAgent(opts.UserAgent), identify.DisableSignedPeerRecord())
 	} else {
-		h.ids, err = identify.NewIDService(
-			h,
-			identify.UserAgent(opts.UserAgent),
-			identify.ProtocolVersion(opts.ProtocolVersion),
-		)
+		h.ids, err = identify.NewIDService(h, identify.UserAgent(opts.UserAgent))
 	}
 	if err != nil {
 		return nil, fmt.Errorf("failed to create Identify service: %s", err)
diff --git a/p2p/protocol/identify/id.go b/p2p/protocol/identify/id.go
index b9d95240..4794619d 100644
--- a/p2p/protocol/identify/id.go
+++ b/p2p/protocol/identify/id.go
@@ -33,7 +33,11 @@ var log = logging.Logger("net/identify")
 // service.
 const ID = "/ipfs/id/1.0.0"
 
-const DefaultProtocolVersion = "ipfs/0.1.0"
+// LibP2PVersion holds the current protocol version for a client running this code
+// TODO(jbenet): fix the versioning mess.
+// XXX: Don't change this till 2020. You'll break all go-ipfs versions prior to
+// 0.4.17 which asserted an exact version match.
+const LibP2PVersion = "ipfs/0.1.0"
 
 const ServiceName = "libp2p.identify"
 
@@ -85,9 +89,8 @@ type IDService interface {
 //   - Our IPFS Agent Version
 //   - Our public Listen Addresses
 type idService struct {
-	Host            host.Host
-	UserAgent       string
-	ProtocolVersion string
+	Host      host.Host
+	UserAgent string
 
 	ctx       context.Context
 	ctxCancel context.CancelFunc
@@ -132,15 +135,9 @@ func NewIDService(h host.Host, opts ...Option) (*idService, error) {
 		userAgent = cfg.userAgent
 	}
 
-	protocolVersion := DefaultProtocolVersion
-	if cfg.protocolVersion != "" {
-		protocolVersion = cfg.protocolVersion
-	}
-
 	s := &idService{
-		Host:            h,
-		UserAgent:       userAgent,
-		ProtocolVersion: protocolVersion,
+		Host:      h,
+		UserAgent: userAgent,
 
 		conns: make(map[network.Conn]chan struct{}),
 
@@ -191,10 +188,8 @@ func (ids *idService) loop() {
 	defer ids.refCount.Done()
 
 	phs := make(map[peer.ID]*peerHandler)
-	sub, err := ids.Host.EventBus().Subscribe([]interface{}{
-		&event.EvtLocalProtocolsUpdated{},
-		&event.EvtLocalAddressesUpdated{},
-	}, eventbus.BufSize(256))
+	sub, err := ids.Host.EventBus().Subscribe([]interface{}{&event.EvtLocalProtocolsUpdated{},
+		&event.EvtLocalAddressesUpdated{}}, eventbus.BufSize(256))
 	if err != nil {
 		log.Errorf("failed to subscribe to events on the bus, err=%s", err)
 		return
@@ -494,6 +489,7 @@ func (ids *idService) writeChunkedIdentifyMsg(c network.Conn, snapshot *identify
 	m := &pb.Identify{SignedPeerRecord: sr}
 	err := writer.WriteMsg(m)
 	return err
+
 }
 
 func (ids *idService) createBaseIdentifyResponse(
@@ -545,8 +541,10 @@ func (ids *idService) createBaseIdentifyResponse(
 	}
 
 	// set protocol versions
-	mes.ProtocolVersion = &ids.ProtocolVersion
-	mes.AgentVersion = &ids.UserAgent
+	pv := LibP2PVersion
+	av := ids.UserAgent
+	mes.ProtocolVersion = &pv
+	mes.AgentVersion = &av
 
 	return mes
 }
diff --git a/p2p/protocol/identify/opts.go b/p2p/protocol/identify/opts.go
index 38f505b6..0eb1d966 100644
--- a/p2p/protocol/identify/opts.go
+++ b/p2p/protocol/identify/opts.go
@@ -1,7 +1,6 @@
 package identify
 
 type config struct {
-	protocolVersion         string
 	userAgent               string
 	disableSignedPeerRecord bool
 }
@@ -9,14 +8,6 @@ type config struct {
 // Option is an option function for identify.
 type Option func(*config)
 
-// ProtocolVersion sets the protocol version string that will be used to
-// identify the family of protocols used by the peer.
-func ProtocolVersion(s string) Option {
-	return func(cfg *config) {
-		cfg.protocolVersion = s
-	}
-}
-
 // UserAgent sets the user agent this node will identify itself with to peers.
 func UserAgent(ua string) Option {
 	return func(cfg *config) {
diff --git a/p2p/security/noise/handshake.go b/p2p/security/noise/handshake.go
index b71d7ef8..bb68196c 100644
--- a/p2p/security/noise/handshake.go
+++ b/p2p/security/noise/handshake.go
@@ -10,17 +10,16 @@ import (
 	"runtime/debug"
 	"time"
 
-	"github.com/minio/sha256-simd"
 	"golang.org/x/crypto/chacha20poly1305"
 
-	"github.com/libp2p/go-libp2p/p2p/security/noise/pb"
-
 	"github.com/libp2p/go-libp2p/core/crypto"
 	"github.com/libp2p/go-libp2p/core/peer"
+	"github.com/libp2p/go-libp2p/p2p/security/noise/pb"
 
 	"github.com/flynn/noise"
 	"github.com/gogo/protobuf/proto"
 	pool "github.com/libp2p/go-buffer-pool"
+	"github.com/minio/sha256-simd"
 )
 
 // payloadSigPrefix is prepended to our Noise static key before signing with
@@ -89,10 +88,12 @@ func (s *secureSession) runHandshake(ctx context.Context) (err error) {
 
 	if s.initiator {
 		// stage 0 //
-		// do not send the payload just yet, as it would be plaintext; not secret.
 		// Handshake Msg Len = len(DH ephemeral key)
-		err = s.sendHandshakeMessage(hs, nil, hbuf)
-		if err != nil {
+		var ed []byte
+		if s.earlyDataHandler != nil {
+			ed = s.earlyDataHandler.Send(ctx, s.insecureConn, s.remoteID)
+		}
+		if err := s.sendHandshakeMessage(hs, ed, hbuf); err != nil {
 			return fmt.Errorf("error sending handshake message: %w", err)
 		}
 
@@ -101,29 +102,34 @@ func (s *secureSession) runHandshake(ctx context.Context) (err error) {
 		if err != nil {
 			return fmt.Errorf("error reading handshake message: %w", err)
 		}
-		err = s.handleRemoteHandshakePayload(plaintext, hs.PeerStatic())
-		if err != nil {
+		if err := s.handleRemoteHandshakePayload(plaintext, hs.PeerStatic()); err != nil {
 			return err
 		}
 
 		// stage 2 //
 		// Handshake Msg Len = len(DHT static key) +  MAC(static key is encrypted) + len(Payload) + MAC(payload is encrypted)
-		err = s.sendHandshakeMessage(hs, payload, hbuf)
-		if err != nil {
+		if err := s.sendHandshakeMessage(hs, payload, hbuf); err != nil {
 			return fmt.Errorf("error sending handshake message: %w", err)
 		}
+		return nil
 	} else {
 		// stage 0 //
-		// We don't expect any payload on the first message.
-		if _, err := s.readHandshakeMessage(hs); err != nil {
+		initialPayload, err := s.readHandshakeMessage(hs)
+		if err != nil {
 			return fmt.Errorf("error reading handshake message: %w", err)
 		}
+		if s.earlyDataHandler != nil {
+			if err := s.earlyDataHandler.Received(ctx, s.insecureConn, initialPayload); err != nil {
+				return err
+			}
+		} else if len(initialPayload) > 0 {
+			return fmt.Errorf("received unexpected early data (%d bytes)", len(initialPayload))
+		}
 
 		// stage 1 //
 		// Handshake Msg Len = len(DH ephemeral key) + len(DHT static key) +  MAC(static key is encrypted) + len(Payload) +
 		// MAC(payload is encrypted)
-		err = s.sendHandshakeMessage(hs, payload, hbuf)
-		if err != nil {
+		if err := s.sendHandshakeMessage(hs, payload, hbuf); err != nil {
 			return fmt.Errorf("error sending handshake message: %w", err)
 		}
 
@@ -132,13 +138,8 @@ func (s *secureSession) runHandshake(ctx context.Context) (err error) {
 		if err != nil {
 			return fmt.Errorf("error reading handshake message: %w", err)
 		}
-		err = s.handleRemoteHandshakePayload(plaintext, hs.PeerStatic())
-		if err != nil {
-			return err
-		}
+		return s.handleRemoteHandshakePayload(plaintext, hs.PeerStatic())
 	}
-
-	return nil
 }
 
 // setCipherStates sets the initial cipher states that will be used to protect
diff --git a/p2p/security/noise/session.go b/p2p/security/noise/session.go
index a563e58b..5e3d0956 100644
--- a/p2p/security/noise/session.go
+++ b/p2p/security/noise/session.go
@@ -36,20 +36,22 @@ type secureSession struct {
 	dec *noise.CipherState
 
 	// noise prologue
-	prologue []byte
+	prologue         []byte
+	earlyDataHandler EarlyDataHandler
 }
 
 // newSecureSession creates a Noise session over the given insecureConn Conn, using
 // the libp2p identity keypair from the given Transport.
-func newSecureSession(tpt *Transport, ctx context.Context, insecure net.Conn, remote peer.ID, prologue []byte, initiator bool) (*secureSession, error) {
+func newSecureSession(tpt *Transport, ctx context.Context, insecure net.Conn, remote peer.ID, prologue []byte, edh EarlyDataHandler, initiator bool) (*secureSession, error) {
 	s := &secureSession{
-		insecureConn:   insecure,
-		insecureReader: bufio.NewReader(insecure),
-		initiator:      initiator,
-		localID:        tpt.localID,
-		localKey:       tpt.privateKey,
-		remoteID:       remote,
-		prologue:       prologue,
+		insecureConn:     insecure,
+		insecureReader:   bufio.NewReader(insecure),
+		initiator:        initiator,
+		localID:          tpt.localID,
+		localKey:         tpt.privateKey,
+		remoteID:         remote,
+		prologue:         prologue,
+		earlyDataHandler: edh,
 	}
 
 	// the go-routine we create to run the handshake will
diff --git a/p2p/security/noise/session_transport.go b/p2p/security/noise/session_transport.go
index b4414136..973f6fac 100644
--- a/p2p/security/noise/session_transport.go
+++ b/p2p/security/noise/session_transport.go
@@ -12,6 +12,32 @@ import (
 
 type SessionOption = func(*SessionTransport) error
 
+// Prologue sets a prologue for the Noise session.
+// The handshake will only complete successfully if both parties set the same prologue.
+// See https://noiseprotocol.org/noise.html#prologue for details.
+func Prologue(prologue []byte) SessionOption {
+	return func(s *SessionTransport) error {
+		s.prologue = prologue
+		return nil
+	}
+}
+
+// EarlyDataHandler allows attaching an (unencrypted) application payload to the first handshake message.
+// While unencrypted, the integrity of this early data is retroactively authenticated on completion of the handshake.
+type EarlyDataHandler interface {
+	// Send is called for the client before sending the first handshake message.
+	Send(context.Context, net.Conn, peer.ID) []byte
+	// Received is called for the server when the first handshake message from the client is received.
+	Received(context.Context, net.Conn, []byte) error
+}
+
+func EarlyData(h EarlyDataHandler) SessionOption {
+	return func(s *SessionTransport) error {
+		s.earlyDataHandler = h
+		return nil
+	}
+}
+
 var _ sec.SecureTransport = &SessionTransport{}
 
 // SessionTransport can be used
@@ -19,13 +45,14 @@ var _ sec.SecureTransport = &SessionTransport{}
 type SessionTransport struct {
 	t *Transport
 	// options
-	prologue []byte
+	prologue         []byte
+	earlyDataHandler EarlyDataHandler
 }
 
 // SecureInbound runs the Noise handshake as the responder.
 // If p is empty, connections from any peer are accepted.
 func (i *SessionTransport) SecureInbound(ctx context.Context, insecure net.Conn, p peer.ID) (sec.SecureConn, error) {
-	c, err := newSecureSession(i.t, ctx, insecure, p, i.prologue, false)
+	c, err := newSecureSession(i.t, ctx, insecure, p, i.prologue, i.earlyDataHandler, false)
 	if err != nil {
 		addr, maErr := manet.FromNetAddr(insecure.RemoteAddr())
 		if maErr == nil {
@@ -37,22 +64,5 @@ func (i *SessionTransport) SecureInbound(ctx context.Context, insecure net.Conn,
 
 // SecureOutbound runs the Noise handshake as the initiator.
 func (i *SessionTransport) SecureOutbound(ctx context.Context, insecure net.Conn, p peer.ID) (sec.SecureConn, error) {
-	return newSecureSession(i.t, ctx, insecure, p, i.prologue, true)
-}
-
-func (t *Transport) WithSessionOptions(opts ...SessionOption) (sec.SecureTransport, error) {
-	st := &SessionTransport{t: t}
-	for _, opt := range opts {
-		if err := opt(st); err != nil {
-			return nil, err
-		}
-	}
-	return st, nil
-}
-
-func Prologue(prologue []byte) SessionOption {
-	return func(s *SessionTransport) error {
-		s.prologue = prologue
-		return nil
-	}
+	return newSecureSession(i.t, ctx, insecure, p, i.prologue, i.earlyDataHandler, true)
 }
diff --git a/p2p/security/noise/transport.go b/p2p/security/noise/transport.go
index f935b82a..bd66d0fd 100644
--- a/p2p/security/noise/transport.go
+++ b/p2p/security/noise/transport.go
@@ -41,7 +41,7 @@ func New(privkey crypto.PrivKey) (*Transport, error) {
 // SecureInbound runs the Noise handshake as the responder.
 // If p is empty, connections from any peer are accepted.
 func (t *Transport) SecureInbound(ctx context.Context, insecure net.Conn, p peer.ID) (sec.SecureConn, error) {
-	c, err := newSecureSession(t, ctx, insecure, p, nil, false)
+	c, err := newSecureSession(t, ctx, insecure, p, nil, nil, false)
 	if err != nil {
 		addr, maErr := manet.FromNetAddr(insecure.RemoteAddr())
 		if maErr == nil {
@@ -53,5 +53,15 @@ func (t *Transport) SecureInbound(ctx context.Context, insecure net.Conn, p peer
 
 // SecureOutbound runs the Noise handshake as the initiator.
 func (t *Transport) SecureOutbound(ctx context.Context, insecure net.Conn, p peer.ID) (sec.SecureConn, error) {
-	return newSecureSession(t, ctx, insecure, p, nil, true)
+	return newSecureSession(t, ctx, insecure, p, nil, nil, true)
+}
+
+func (t *Transport) WithSessionOptions(opts ...SessionOption) (sec.SecureTransport, error) {
+	st := &SessionTransport{t: t}
+	for _, opt := range opts {
+		if err := opt(st); err != nil {
+			return nil, err
+		}
+	}
+	return st, nil
 }


Test patch:
diff --git a/p2p/protocol/identify/id_test.go b/p2p/protocol/identify/id_test.go
index ebdb6349..d86fc915 100644
--- a/p2p/protocol/identify/id_test.go
+++ b/p2p/protocol/identify/id_test.go
@@ -75,7 +75,7 @@ func testHasProtocolVersions(t *testing.T, h host.Host, p peer.ID) {
 		t.Error("no protocol version")
 		return
 	}
-	if v.(string) != identify.DefaultProtocolVersion {
+	if v.(string) != identify.LibP2PVersion {
 		t.Error("protocol mismatch", err)
 	}
 	v, err = h.Peerstore().Get(p, "AgentVersion")
diff --git a/p2p/security/noise/crypto_test.go b/p2p/security/noise/crypto_test.go
index 35837a5a..9b7d3908 100644
--- a/p2p/security/noise/crypto_test.go
+++ b/p2p/security/noise/crypto_test.go
@@ -93,7 +93,7 @@ func TestCryptoFailsIfHandshakeIncomplete(t *testing.T) {
 	init, resp := net.Pipe()
 	_ = resp.Close()
 
-	session, _ := newSecureSession(initTransport, context.TODO(), init, "remote-peer", nil, true)
+	session, _ := newSecureSession(initTransport, context.TODO(), init, "remote-peer", nil, nil, true)
 	_, err := session.encrypt(nil, []byte("hi"))
 	if err == nil {
 		t.Error("expected encryption error when handshake incomplete")
diff --git a/p2p/security/noise/transport_test.go b/p2p/security/noise/transport_test.go
index 5269be29..108efec8 100644
--- a/p2p/security/noise/transport_test.go
+++ b/p2p/security/noise/transport_test.go
@@ -78,10 +78,10 @@ func connect(t *testing.T, initTransport, respTransport *Transport) (*secureSess
 	done := make(chan struct{})
 	go func() {
 		defer close(done)
-		initConn, initErr = initTransport.SecureOutbound(context.TODO(), init, respTransport.localID)
+		initConn, initErr = initTransport.SecureOutbound(context.Background(), init, respTransport.localID)
 	}()
 
-	respConn, respErr := respTransport.SecureInbound(context.TODO(), resp, "")
+	respConn, respErr := respTransport.SecureInbound(context.Background(), resp, "")
 	<-done
 
 	if initErr != nil {
@@ -171,7 +171,7 @@ func TestPeerIDMatch(t *testing.T) {
 	done := make(chan struct{})
 	go func() {
 		defer close(done)
-		conn, err := initTransport.SecureOutbound(context.TODO(), init, respTransport.localID)
+		conn, err := initTransport.SecureOutbound(context.Background(), init, respTransport.localID)
 		assert.NoError(t, err)
 		assert.Equal(t, conn.RemotePeer(), respTransport.localID)
 		b := make([]byte, 6)
@@ -180,7 +180,7 @@ func TestPeerIDMatch(t *testing.T) {
 		assert.Equal(t, b, []byte("foobar"))
 	}()
 
-	conn, err := respTransport.SecureInbound(context.TODO(), resp, initTransport.localID)
+	conn, err := respTransport.SecureInbound(context.Background(), resp, initTransport.localID)
 	require.NoError(t, err)
 	require.Equal(t, conn.RemotePeer(), initTransport.localID)
 	_, err = conn.Write([]byte("foobar"))
@@ -194,11 +194,11 @@ func TestPeerIDMismatchOutboundFailsHandshake(t *testing.T) {
 
 	errChan := make(chan error)
 	go func() {
-		_, err := initTransport.SecureOutbound(context.TODO(), init, "a-random-peer-id")
+		_, err := initTransport.SecureOutbound(context.Background(), init, "a-random-peer-id")
 		errChan <- err
 	}()
 
-	_, err := respTransport.SecureInbound(context.TODO(), resp, "")
+	_, err := respTransport.SecureInbound(context.Background(), resp, "")
 	require.Error(t, err)
 
 	initErr := <-errChan
@@ -214,13 +214,13 @@ func TestPeerIDMismatchInboundFailsHandshake(t *testing.T) {
 	done := make(chan struct{})
 	go func() {
 		defer close(done)
-		conn, err := initTransport.SecureOutbound(context.TODO(), init, respTransport.localID)
+		conn, err := initTransport.SecureOutbound(context.Background(), init, respTransport.localID)
 		assert.NoError(t, err)
 		_, err = conn.Read([]byte{0})
 		assert.Error(t, err)
 	}()
 
-	_, err := respTransport.SecureInbound(context.TODO(), resp, "a-random-peer-id")
+	_, err := respTransport.SecureInbound(context.Background(), resp, "a-random-peer-id")
 	require.Error(t, err, "expected responder to fail with peer ID mismatch error")
 	<-done
 }
@@ -387,7 +387,7 @@ func TestPrologueMatches(t *testing.T) {
 		tpt, err := initTransport.
 			WithSessionOptions(Prologue(commonPrologue))
 		require.NoError(t, err)
-		conn, err := tpt.SecureOutbound(context.TODO(), initConn, respTransport.localID)
+		conn, err := tpt.SecureOutbound(context.Background(), initConn, respTransport.localID)
 		require.NoError(t, err)
 		defer conn.Close()
 	}()
@@ -395,7 +395,7 @@ func TestPrologueMatches(t *testing.T) {
 	tpt, err := respTransport.
 		WithSessionOptions(Prologue(commonPrologue))
 	require.NoError(t, err)
-	conn, err := tpt.SecureInbound(context.TODO(), respConn, "")
+	conn, err := tpt.SecureInbound(context.Background(), respConn, "")
 	require.NoError(t, err)
 	defer conn.Close()
 	<-done
@@ -415,14 +415,125 @@ func TestPrologueDoesNotMatchFailsHandshake(t *testing.T) {
 		tpt, err := initTransport.
 			WithSessionOptions(Prologue(initPrologue))
 		require.NoError(t, err)
-		_, err = tpt.SecureOutbound(context.TODO(), initConn, respTransport.localID)
+		_, err = tpt.SecureOutbound(context.Background(), initConn, respTransport.localID)
 		require.Error(t, err)
 	}()
 
 	tpt, err := respTransport.WithSessionOptions(Prologue(respPrologue))
 	require.NoError(t, err)
 
-	_, err = tpt.SecureInbound(context.TODO(), respConn, "")
+	_, err = tpt.SecureInbound(context.Background(), respConn, "")
 	require.Error(t, err)
 	<-done
 }
+
+type earlyDataHandler struct {
+	send     func(context.Context, net.Conn, peer.ID) []byte
+	received func(context.Context, net.Conn, []byte) error
+}
+
+func (e *earlyDataHandler) Send(ctx context.Context, conn net.Conn, id peer.ID) []byte {
+	if e.send == nil {
+		return nil
+	}
+	return e.send(ctx, conn, id)
+}
+
+func (e *earlyDataHandler) Received(ctx context.Context, conn net.Conn, data []byte) error {
+	if e.received == nil {
+		return nil
+	}
+	return e.received(ctx, conn, data)
+}
+
+func TestEarlyDataAccepted(t *testing.T) {
+	var receivedEarlyData []byte
+	serverEDH := &earlyDataHandler{
+		received: func(_ context.Context, _ net.Conn, data []byte) error {
+			receivedEarlyData = data
+			return nil
+		},
+	}
+	clientEDH := &earlyDataHandler{
+		send: func(ctx context.Context, conn net.Conn, id peer.ID) []byte { return []byte("foobar") },
+	}
+	initTransport, err := newTestTransport(t, crypto.Ed25519, 2048).WithSessionOptions(EarlyData(clientEDH))
+	require.NoError(t, err)
+	tpt := newTestTransport(t, crypto.Ed25519, 2048)
+	respTransport, err := tpt.WithSessionOptions(EarlyData(serverEDH))
+	require.NoError(t, err)
+
+	initConn, respConn := newConnPair(t)
+
+	errChan := make(chan error)
+	go func() {
+		_, err := respTransport.SecureInbound(context.Background(), initConn, "")
+		errChan <- err
+	}()
+
+	conn, err := initTransport.SecureOutbound(context.Background(), respConn, tpt.localID)
+	require.NoError(t, err)
+	defer conn.Close()
+
+	require.Equal(t, []byte("foobar"), receivedEarlyData)
+}
+
+func TestEarlyDataRejected(t *testing.T) {
+	serverEDH := &earlyDataHandler{
+		received: func(_ context.Context, _ net.Conn, data []byte) error { return errors.New("nope") },
+	}
+	clientEDH := &earlyDataHandler{
+		send: func(ctx context.Context, conn net.Conn, id peer.ID) []byte { return []byte("foobar") },
+	}
+	initTransport, err := newTestTransport(t, crypto.Ed25519, 2048).WithSessionOptions(EarlyData(clientEDH))
+	require.NoError(t, err)
+	tpt := newTestTransport(t, crypto.Ed25519, 2048)
+	respTransport, err := tpt.WithSessionOptions(EarlyData(serverEDH))
+	require.NoError(t, err)
+
+	initConn, respConn := newConnPair(t)
+
+	errChan := make(chan error)
+	go func() {
+		_, err := respTransport.SecureInbound(context.Background(), initConn, "")
+		errChan <- err
+	}()
+
+	_, err = initTransport.SecureOutbound(context.Background(), respConn, tpt.localID)
+	require.Error(t, err)
+
+	select {
+	case <-time.After(500 * time.Millisecond):
+		t.Fatal("timeout")
+	case err := <-errChan:
+		require.EqualError(t, err, "nope")
+	}
+}
+
+func TestEarlyDataRejectedWithNoHandler(t *testing.T) {
+	clientEDH := &earlyDataHandler{
+		send: func(ctx context.Context, conn net.Conn, id peer.ID) []byte { return []byte("foobar") },
+	}
+	initTransport, err := newTestTransport(t, crypto.Ed25519, 2048).WithSessionOptions(EarlyData(clientEDH))
+	require.NoError(t, err)
+	respTransport := newTestTransport(t, crypto.Ed25519, 2048)
+
+	initConn, respConn := newConnPair(t)
+
+	errChan := make(chan error)
+	go func() {
+		_, err := respTransport.SecureInbound(context.Background(), initConn, "")
+		errChan <- err
+	}()
+
+	_, err = initTransport.SecureOutbound(context.Background(), respConn, respTransport.localID)
+	require.Error(t, err)
+
+	select {
+	case <-time.After(500 * time.Millisecond):
+		t.Fatal("timeout")
+	case err := <-errChan:
+		require.Error(t, err)
+		require.Contains(t, err.Error(), "received unexpected early data")
+	}
+}