package noise import ( "bytes" "context" "encoding/binary" "errors" "io" "math/rand" "net" "testing" "time" "golang.org/x/crypto/chacha20poly1305" "github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/core/protocol" "github.com/libp2p/go-libp2p/core/sec" "github.com/libp2p/go-libp2p/p2p/security/noise/pb" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func newTestTransport(t *testing.T, typ, bits int) *Transport { priv, pub, err := crypto.GenerateKeyPair(typ, bits) if err != nil { t.Fatal(err) } id, err := peer.IDFromPublicKey(pub) if err != nil { t.Fatal(err) } return &Transport{ localID: id, privateKey: priv, } } func newTestTransportWithMuxers(t *testing.T, typ, bits int, muxers []protocol.ID) *Transport { transport := newTestTransport(t, typ, bits) transport.muxers = muxers return transport } // Create a new pair of connected TCP sockets. func newConnPair(t *testing.T) (net.Conn, net.Conn) { lstnr, err := net.Listen("tcp", "localhost:0") if err != nil { t.Fatalf("Failed to listen: %v", err) return nil, nil } var clientErr error var client net.Conn addr := lstnr.Addr() done := make(chan struct{}) go func() { defer close(done) client, clientErr = net.Dial(addr.Network(), addr.String()) }() server, err := lstnr.Accept() <-done lstnr.Close() if err != nil { t.Fatalf("Failed to accept: %v", err) } if clientErr != nil { t.Fatalf("Failed to connect: %v", clientErr) } return client, server } func connect(t *testing.T, initTransport, respTransport *Transport) (*secureSession, *secureSession) { init, resp := newConnPair(t) var initConn sec.SecureConn var initErr error done := make(chan struct{}) go func() { defer close(done) initConn, initErr = initTransport.SecureOutbound(context.Background(), init, respTransport.localID) }() respConn, respErr := respTransport.SecureInbound(context.Background(), resp, "") <-done if initErr != nil { t.Fatal(initErr) } if respErr != nil { t.Fatal(respErr) } return initConn.(*secureSession), respConn.(*secureSession) } func TestDeadlines(t *testing.T) { initTransport := newTestTransport(t, crypto.Ed25519, 2048) respTransport := newTestTransport(t, crypto.Ed25519, 2048) init, resp := newConnPair(t) defer init.Close() defer resp.Close() ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() _, err := initTransport.SecureOutbound(ctx, init, respTransport.localID) if err == nil { t.Fatalf("expected i/o timeout err; got: %s", err) } var neterr net.Error if ok := errors.As(err, &neterr); !ok || !neterr.Timeout() { t.Fatalf("expected i/o timeout err; got: %s", err) } } func TestIDs(t *testing.T) { initTransport := newTestTransport(t, crypto.Ed25519, 2048) respTransport := newTestTransport(t, crypto.Ed25519, 2048) initConn, respConn := connect(t, initTransport, respTransport) defer initConn.Close() defer respConn.Close() if initConn.LocalPeer() != initTransport.localID { t.Fatal("Initiator Local Peer ID mismatch.") } if respConn.RemotePeer() != initTransport.localID { t.Fatal("Responder Remote Peer ID mismatch.") } if initConn.LocalPeer() != respConn.RemotePeer() { t.Fatal("Responder Local Peer ID mismatch.") } // TODO: check after stage 0 of handshake if updated if initConn.RemotePeer() != respTransport.localID { t.Errorf("Initiator Remote Peer ID mismatch. expected %x got %x", respTransport.localID, initConn.RemotePeer()) } } func TestKeys(t *testing.T) { initTransport := newTestTransport(t, crypto.Ed25519, 2048) respTransport := newTestTransport(t, crypto.Ed25519, 2048) initConn, respConn := connect(t, initTransport, respTransport) defer initConn.Close() defer respConn.Close() pk1 := respConn.RemotePublicKey() pk2 := initTransport.privateKey.GetPublic() if !pk1.Equals(pk2) { t.Errorf("Public key mismatch. expected %x got %x", pk1, pk2) } pk3 := initConn.RemotePublicKey() pk4 := respTransport.privateKey.GetPublic() if !pk3.Equals(pk4) { t.Errorf("Public key mismatch. expected %x got %x", pk3, pk4) } } func TestPeerIDMatch(t *testing.T) { initTransport := newTestTransport(t, crypto.Ed25519, 2048) respTransport := newTestTransport(t, crypto.Ed25519, 2048) init, resp := newConnPair(t) done := make(chan struct{}) go func() { defer close(done) conn, err := initTransport.SecureOutbound(context.Background(), init, respTransport.localID) assert.NoError(t, err) assert.Equal(t, conn.RemotePeer(), respTransport.localID) b := make([]byte, 6) _, err = conn.Read(b) assert.NoError(t, err) assert.Equal(t, b, []byte("foobar")) }() 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")) require.NoError(t, err) } func TestPeerIDMismatchOutboundFailsHandshake(t *testing.T) { initTransport := newTestTransport(t, crypto.Ed25519, 2048) respTransport := newTestTransport(t, crypto.Ed25519, 2048) init, resp := newConnPair(t) errChan := make(chan error) go func() { _, err := initTransport.SecureOutbound(context.Background(), init, "a-random-peer-id") errChan <- err }() _, err := respTransport.SecureInbound(context.Background(), resp, "") require.Error(t, err) initErr := <-errChan require.Error(t, initErr, "expected initiator to fail with peer ID mismatch error") require.Contains(t, initErr.Error(), "but remote key matches") } func TestPeerIDMismatchInboundFailsHandshake(t *testing.T) { initTransport := newTestTransport(t, crypto.Ed25519, 2048) respTransport := newTestTransport(t, crypto.Ed25519, 2048) init, resp := newConnPair(t) done := make(chan struct{}) go func() { defer close(done) 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.Background(), resp, "a-random-peer-id") require.Error(t, err, "expected responder to fail with peer ID mismatch error") <-done } func TestPeerIDInboundCheckDisabled(t *testing.T) { initTransport := newTestTransport(t, crypto.Ed25519, 2048) respTransport := newTestTransport(t, crypto.Ed25519, 2048) init, resp := newConnPair(t) initSessionTransport, err := initTransport.WithSessionOptions(DisablePeerIDCheck()) require.NoError(t, err) errChan := make(chan error) go func() { _, err := initSessionTransport.SecureInbound(context.Background(), init, "test") errChan <- err }() _, err = respTransport.SecureOutbound(context.Background(), resp, initTransport.localID) require.NoError(t, err) initErr := <-errChan require.NoError(t, initErr) } func TestPeerIDOutboundNoCheck(t *testing.T) { initTransport := newTestTransport(t, crypto.Ed25519, 2048) respTransport := newTestTransport(t, crypto.Ed25519, 2048) init, resp := newConnPair(t) initSessionTransport, err := initTransport.WithSessionOptions(DisablePeerIDCheck()) require.NoError(t, err) errChan := make(chan error) go func() { _, err := initSessionTransport.SecureOutbound(context.Background(), init, "test") errChan <- err }() _, err = respTransport.SecureInbound(context.Background(), resp, "") require.NoError(t, err) initErr := <-errChan require.NoError(t, initErr) } func TestLargePayloads(t *testing.T) { initTransport := newTestTransport(t, crypto.Ed25519, 2048) respTransport := newTestTransport(t, crypto.Ed25519, 2048) initConn, respConn := connect(t, initTransport, respTransport) defer initConn.Close() defer respConn.Close() // enough to require a couple Noise messages, with a size that // isn't a neat multiple of Noise message size, just in case rnd := rand.New(rand.NewSource(1234)) const size = 100000 before := make([]byte, size) rnd.Read(before) if _, err := initConn.Write(before); err != nil { t.Fatal(err) } after := make([]byte, len(before)) afterLen, err := io.ReadFull(respConn, after) if err != nil { t.Fatal(err) } if len(before) != afterLen { t.Errorf("expected to read same amount of data as written. written=%d read=%d", len(before), afterLen) } if !bytes.Equal(before, after) { t.Error("Message mismatch.") } } // Tests XX handshake func TestHandshakeXX(t *testing.T) { initTransport := newTestTransport(t, crypto.Ed25519, 2048) respTransport := newTestTransport(t, crypto.Ed25519, 2048) initConn, respConn := connect(t, initTransport, respTransport) defer initConn.Close() defer respConn.Close() before := []byte("hello world") _, err := initConn.Write(before) if err != nil { t.Fatal(err) } after := make([]byte, len(before)) _, err = respConn.Read(after) if err != nil { t.Fatal(err) } if !bytes.Equal(before, after) { t.Errorf("Message mismatch. %v != %v", before, after) } } func TestBufferEqEncPayload(t *testing.T) { initTransport := newTestTransport(t, crypto.Ed25519, 2048) respTransport := newTestTransport(t, crypto.Ed25519, 2048) initConn, respConn := connect(t, initTransport, respTransport) defer initConn.Close() defer respConn.Close() before := []byte("hello world") _, err := initConn.Write(before) require.NoError(t, err) after := make([]byte, len(before)+chacha20poly1305.Overhead) afterLen, err := respConn.Read(after) require.NoError(t, err) require.Equal(t, len(before), afterLen) require.Equal(t, before, after[:len(before)]) } func TestBufferEqDecryptedPayload(t *testing.T) { initTransport := newTestTransport(t, crypto.Ed25519, 2048) respTransport := newTestTransport(t, crypto.Ed25519, 2048) initConn, respConn := connect(t, initTransport, respTransport) defer initConn.Close() defer respConn.Close() before := []byte("hello world") _, err := initConn.Write(before) require.NoError(t, err) after := make([]byte, len(before)+1) afterLen, err := respConn.Read(after) require.NoError(t, err) require.Equal(t, len(before), afterLen) require.Equal(t, before, after[:len(before)]) } func TestReadUnencryptedFails(t *testing.T) { // case1 buffer > len(msg) initTransport := newTestTransport(t, crypto.Ed25519, 2048) respTransport := newTestTransport(t, crypto.Ed25519, 2048) initConn, respConn := connect(t, initTransport, respTransport) defer initConn.Close() defer respConn.Close() before := []byte("hello world") msg := make([]byte, len(before)+LengthPrefixLength) binary.BigEndian.PutUint16(msg, uint16(len(before))) copy(msg[LengthPrefixLength:], before) n, err := initConn.insecureConn.Write(msg) require.NoError(t, err) require.Equal(t, len(msg), n) after := make([]byte, len(msg)+1) afterLen, err := respConn.Read(after) require.Error(t, err) require.Equal(t, 0, afterLen) // case2: buffer < len(msg) initTransport = newTestTransport(t, crypto.Ed25519, 2048) respTransport = newTestTransport(t, crypto.Ed25519, 2048) initConn, respConn = connect(t, initTransport, respTransport) defer initConn.Close() defer respConn.Close() before = []byte("hello world") msg = make([]byte, len(before)+LengthPrefixLength) binary.BigEndian.PutUint16(msg, uint16(len(before))) copy(msg[LengthPrefixLength:], before) n, err = initConn.insecureConn.Write(msg) require.NoError(t, err) require.Equal(t, len(msg), n) after = make([]byte, 1) afterLen, err = respConn.Read(after) require.Error(t, err) require.Equal(t, 0, afterLen) } func TestPrologueMatches(t *testing.T) { commonPrologue := []byte("test") initTransport := newTestTransport(t, crypto.Ed25519, 2048) respTransport := newTestTransport(t, crypto.Ed25519, 2048) initConn, respConn := newConnPair(t) done := make(chan struct{}) go func() { defer close(done) tpt, err := initTransport. WithSessionOptions(Prologue(commonPrologue)) require.NoError(t, err) conn, err := tpt.SecureOutbound(context.Background(), initConn, respTransport.localID) require.NoError(t, err) defer conn.Close() }() tpt, err := respTransport. WithSessionOptions(Prologue(commonPrologue)) require.NoError(t, err) conn, err := tpt.SecureInbound(context.Background(), respConn, "") require.NoError(t, err) defer conn.Close() <-done } func TestPrologueDoesNotMatchFailsHandshake(t *testing.T) { initPrologue, respPrologue := []byte("initPrologue"), []byte("respPrologue") initTransport := newTestTransport(t, crypto.Ed25519, 2048) respTransport := newTestTransport(t, crypto.Ed25519, 2048) initConn, respConn := newConnPair(t) done := make(chan struct{}) go func() { defer close(done) tpt, err := initTransport. WithSessionOptions(Prologue(initPrologue)) require.NoError(t, err) _, 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.Background(), respConn, "") require.Error(t, err) <-done } type earlyDataHandler struct { send func(context.Context, net.Conn, peer.ID) *pb.NoiseExtensions received func(context.Context, net.Conn, *pb.NoiseExtensions) error } func (e *earlyDataHandler) Send(ctx context.Context, conn net.Conn, id peer.ID) *pb.NoiseExtensions { if e.send == nil { return nil } return e.send(ctx, conn, id) } func (e *earlyDataHandler) Received(ctx context.Context, conn net.Conn, ext *pb.NoiseExtensions) error { if e.received == nil { return nil } return e.received(ctx, conn, ext) } func TestEarlyDataAccepted(t *testing.T) { handshake := func(t *testing.T, client, server EarlyDataHandler) { t.Helper() initTransport, err := newTestTransport(t, crypto.Ed25519, 2048).WithSessionOptions(EarlyData(client, nil)) require.NoError(t, err) tpt := newTestTransport(t, crypto.Ed25519, 2048) respTransport, err := tpt.WithSessionOptions(EarlyData(nil, server)) 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) select { case <-time.After(500 * time.Millisecond): t.Fatal("timeout") case err := <-errChan: require.NoError(t, err) } defer conn.Close() } var receivedExtensions *pb.NoiseExtensions receivingEDH := &earlyDataHandler{ received: func(_ context.Context, _ net.Conn, ext *pb.NoiseExtensions) error { receivedExtensions = ext return nil }, } sendingEDH := &earlyDataHandler{ send: func(context.Context, net.Conn, peer.ID) *pb.NoiseExtensions { return &pb.NoiseExtensions{WebtransportCerthashes: [][]byte{[]byte("foobar")}} }, } t.Run("client sending", func(t *testing.T) { handshake(t, sendingEDH, receivingEDH) require.Equal(t, [][]byte{[]byte("foobar")}, receivedExtensions.WebtransportCerthashes) receivedExtensions = nil }) t.Run("server sending", func(t *testing.T) { handshake(t, receivingEDH, sendingEDH) require.Equal(t, [][]byte{[]byte("foobar")}, receivedExtensions.WebtransportCerthashes) receivedExtensions = nil }) } func TestEarlyDataRejected(t *testing.T) { handshake := func(t *testing.T, client, server EarlyDataHandler) (clientErr, serverErr error) { initTransport, err := newTestTransport(t, crypto.Ed25519, 2048).WithSessionOptions(EarlyData(client, nil)) require.NoError(t, err) tpt := newTestTransport(t, crypto.Ed25519, 2048) respTransport, err := tpt.WithSessionOptions(EarlyData(nil, server)) require.NoError(t, err) initConn, respConn := newConnPair(t) errChan := make(chan error) go func() { _, err := respTransport.SecureInbound(context.Background(), initConn, "") errChan <- err }() // As early data is sent with the last handshake message, the handshake will appear // to succeed for the client. var conn sec.SecureConn conn, clientErr = initTransport.SecureOutbound(context.Background(), respConn, tpt.localID) if clientErr == nil { _, clientErr = conn.Read([]byte{0}) } select { case <-time.After(500 * time.Millisecond): t.Fatal("timeout") case err := <-errChan: serverErr = err } return } receivingEDH := &earlyDataHandler{ received: func(context.Context, net.Conn, *pb.NoiseExtensions) error { return errors.New("nope") }, } sendingEDH := &earlyDataHandler{ send: func(context.Context, net.Conn, peer.ID) *pb.NoiseExtensions { return &pb.NoiseExtensions{WebtransportCerthashes: [][]byte{[]byte("foobar")}} }, } t.Run("client sending", func(t *testing.T) { clientErr, serverErr := handshake(t, sendingEDH, receivingEDH) require.Error(t, clientErr) require.EqualError(t, serverErr, "nope") }) t.Run("server sending", func(t *testing.T) { clientErr, serverErr := handshake(t, receivingEDH, sendingEDH) require.Error(t, serverErr) require.EqualError(t, clientErr, "nope") }) } func TestEarlyfffDataAcceptedWithNoHandler(t *testing.T) { clientEDH := &earlyDataHandler{ send: func(ctx context.Context, conn net.Conn, id peer.ID) *pb.NoiseExtensions { return &pb.NoiseExtensions{WebtransportCerthashes: [][]byte{[]byte("foobar")}} }, } initTransport, err := newTestTransport(t, crypto.Ed25519, 2048).WithSessionOptions(EarlyData(clientEDH, nil)) 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 }() conn, err := initTransport.SecureOutbound(context.Background(), respConn, respTransport.localID) require.NoError(t, err) defer conn.Close() select { case <-time.After(500 * time.Millisecond): t.Fatal("timeout") case err := <-errChan: require.NoError(t, err) } } type noiseEarlyDataTestCase struct { clientProtos []protocol.ID serverProtos []protocol.ID expectedResult protocol.ID } func TestHandshakeWithTransportEarlyData(t *testing.T) { tests := []noiseEarlyDataTestCase{ { clientProtos: nil, serverProtos: nil, expectedResult: "", }, { clientProtos: []protocol.ID{"muxer1"}, serverProtos: []protocol.ID{"muxer1"}, expectedResult: "muxer1", }, { clientProtos: []protocol.ID{"muxer1"}, serverProtos: []protocol.ID{}, expectedResult: "", }, { clientProtos: []protocol.ID{}, serverProtos: []protocol.ID{"muxer2"}, expectedResult: "", }, { clientProtos: []protocol.ID{"muxer2"}, serverProtos: []protocol.ID{"muxer1"}, expectedResult: "", }, { clientProtos: []protocol.ID{"muxer1", "muxer2"}, serverProtos: []protocol.ID{"muxer2", "muxer1"}, expectedResult: "muxer1", }, { clientProtos: []protocol.ID{"muxer3", "muxer2", "muxer1"}, serverProtos: []protocol.ID{"muxer2", "muxer1"}, expectedResult: "muxer2", }, { clientProtos: []protocol.ID{"muxer1", "muxer2"}, serverProtos: []protocol.ID{"muxer3"}, expectedResult: "", }, } noiseHandshake := func(t *testing.T, initProtos, respProtos []protocol.ID, expectedProto protocol.ID) { initTransport := newTestTransportWithMuxers(t, crypto.Ed25519, 2048, initProtos) respTransport := newTestTransportWithMuxers(t, crypto.Ed25519, 2048, respProtos) initConn, respConn := connect(t, initTransport, respTransport) defer initConn.Close() defer respConn.Close() require.Equal(t, expectedProto, initConn.connectionState.StreamMultiplexer) require.Equal(t, expectedProto != "", initConn.connectionState.UsedEarlyMuxerNegotiation) require.Equal(t, expectedProto, respConn.connectionState.StreamMultiplexer) require.Equal(t, expectedProto != "", respConn.connectionState.UsedEarlyMuxerNegotiation) initData := []byte("Test data for noise transport") _, err := initConn.Write(initData) require.NoError(t, err) respData := make([]byte, len(initData)) _, err = respConn.Read(respData) require.NoError(t, err) require.Equal(t, initData, respData) } for _, test := range tests { t.Run("Transport EarlyData Test", func(t *testing.T) { noiseHandshake(t, test.clientProtos, test.serverProtos, test.expectedResult) }) } }