diff --git a/.drone.yml b/.drone.yml index 0ec4826..01febfc 100644 --- a/.drone.yml +++ b/.drone.yml @@ -21,6 +21,12 @@ pipeline: commands: - export PATH=$PATH:/go/src/cwtch.im/tapir - sh testing/tests.sh + integ-test: + image: golang + commands: + - ./tor -f ./torrc + - sleep 15 + - go test -v cwtch.im/tapir/testing notify-email: image: drillster/drone-email host: build.openprivacy.ca diff --git a/.gitignore b/.gitignore index e621b8b..4ede555 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ vendor/ .idea /tor/ coverage.out +/testing/tor/ diff --git a/applications/auth.go b/applications/auth.go index 2004f5f..f7af141 100644 --- a/applications/auth.go +++ b/applications/auth.go @@ -1,11 +1,10 @@ package applications import ( - "crypto/rand" "crypto/subtle" "cwtch.im/tapir" + "cwtch.im/tapir/primitives" "encoding/json" - "git.openprivacy.ca/openprivacy/libricochet-go/identity" "git.openprivacy.ca/openprivacy/libricochet-go/log" "git.openprivacy.ca/openprivacy/libricochet-go/utils" "golang.org/x/crypto/ed25519" @@ -35,11 +34,8 @@ func (ea AuthApp) NewInstance() tapir.Application { // or the connection is closed. func (ea AuthApp) Init(connection tapir.Connection) { longTermPubKey := ed25519.PublicKey(connection.ID().PublicKeyBytes()) - epk, esk, _ := ed25519.GenerateKey(rand.Reader) - ephemeralPublicKey := ed25519.PublicKey(epk) - ephemeralPrivateKey := ed25519.PrivateKey(esk) - ephemeralIdentity := identity.InitializeV3("", &ephemeralPrivateKey, &ephemeralPublicKey) - authMessage := AuthMessage{LongTermPublicKey: longTermPubKey, EphemeralPublicKey: ephemeralPublicKey} + ephemeralIdentity, _ := primitives.InitializeEphemeral() + authMessage := AuthMessage{LongTermPublicKey: longTermPubKey, EphemeralPublicKey: ephemeralIdentity.PublicKey()} serialized, _ := json.Marshal(authMessage) connection.Send(serialized) message := connection.Expect() @@ -51,23 +47,8 @@ func (ea AuthApp) Init(connection tapir.Connection) { return } - // 3DH Handshake - l2e := connection.ID().EDH(remoteAuthMessage.EphemeralPublicKey) - e2l := ephemeralIdentity.EDH(remoteAuthMessage.LongTermPublicKey) - e2e := ephemeralIdentity.EDH(remoteAuthMessage.EphemeralPublicKey) - - // We need to define an order for the result concatenation so that both sides derive the same key. - var result [96]byte - if connection.IsOutbound() { - copy(result[0:32], l2e) - copy(result[32:64], e2l) - copy(result[64:96], e2e) - } else { - copy(result[0:32], e2l) - copy(result[32:64], l2e) - copy(result[64:96], e2e) - } - connection.SetEncryptionKey(sha3.Sum256(result[:])) + key := primitives.Perform3DH(connection.ID(), &ephemeralIdentity, remoteAuthMessage.LongTermPublicKey, remoteAuthMessage.EphemeralPublicKey, connection.IsOutbound()) + connection.SetEncryptionKey(key) // Wait to Sync (we need to ensure that both the Local and Remote server have turned encryption on // otherwise our next Send will fail. diff --git a/applications/auth_test.go b/applications/auth_test.go index 7a07781..a00ced7 100644 --- a/applications/auth_test.go +++ b/applications/auth_test.go @@ -2,22 +2,19 @@ package applications import ( "crypto/rand" + "cwtch.im/tapir/primitives" "encoding/json" - "git.openprivacy.ca/openprivacy/libricochet-go/identity" "golang.org/x/crypto/ed25519" "testing" ) type MockConnection struct { - id identity.Identity + id primitives.Identity outbound bool } func (mc *MockConnection) Init(outbound bool) { - pubkey, privateKey, _ := ed25519.GenerateKey(rand.Reader) - sk := ed25519.PrivateKey(privateKey) - pk := ed25519.PublicKey(pubkey) - mc.id = identity.InitializeV3("", &sk, &pk) + mc.id, _ = primitives.InitializeEphemeral() mc.outbound = outbound return } @@ -30,7 +27,7 @@ func (mc MockConnection) IsOutbound() bool { return mc.outbound } -func (mc MockConnection) ID() *identity.Identity { +func (mc MockConnection) ID() *primitives.Identity { return &mc.id } diff --git a/networks/tor/BaseOnionService.go b/networks/tor/BaseOnionService.go index a13a66e..7dc5ecf 100644 --- a/networks/tor/BaseOnionService.go +++ b/networks/tor/BaseOnionService.go @@ -3,10 +3,10 @@ package tor import ( "crypto/rand" "cwtch.im/tapir" + "cwtch.im/tapir/primitives" "encoding/base64" "errors" "git.openprivacy.ca/openprivacy/libricochet-go/connectivity" - "git.openprivacy.ca/openprivacy/libricochet-go/identity" "git.openprivacy.ca/openprivacy/libricochet-go/log" "golang.org/x/crypto/ed25519" "sync" @@ -17,14 +17,14 @@ import ( type BaseOnionService struct { connections sync.Map acn connectivity.ACN - id identity.Identity + id *primitives.Identity privateKey ed25519.PrivateKey ls connectivity.ListenService } // Init initializes a BaseOnionService with a given private key and identity // The private key is needed to initialize the Onion listen socket, ideally we could just pass an Identity in here. -func (s *BaseOnionService) Init(acn connectivity.ACN, sk ed25519.PrivateKey, id identity.Identity) { +func (s *BaseOnionService) Init(acn connectivity.ACN, sk ed25519.PrivateKey, id *primitives.Identity) { // run add onion // get listen context s.acn = acn diff --git a/primitives/3DH.go b/primitives/3DH.go new file mode 100644 index 0000000..084a295 --- /dev/null +++ b/primitives/3DH.go @@ -0,0 +1,35 @@ +package primitives + +import ( + "golang.org/x/crypto/ed25519" + "golang.org/x/crypto/sha3" +) + +// Perform3DH encapsulates a triple-diffie-hellman key exchange. +// In this exchange Alice and Bob both hold longterm identity keypairs +// Both Alice and Bob generate an additional ephemeral key pair: +// 3 Diffie Hellman exchanges are then performed: +// Alice Long Term <-> Bob Ephemeral +// Alice Ephemeral <-> Bob Long Term +// Alice Ephemeral <-> Bob Ephemeral +// +// Through this, a unique session key is derived. The exchange is offline-deniable (in the context of Tapir and Onion Service) +func Perform3DH(longtermIdentity *Identity, ephemeralIdentity *Identity, remoteLongTermPublicKey ed25519.PublicKey, remoteEphemeralPublicKey ed25519.PublicKey, outbound bool) [32]byte { + // 3DH Handshake + l2e := longtermIdentity.EDH(remoteEphemeralPublicKey) + e2l := ephemeralIdentity.EDH(remoteLongTermPublicKey) + e2e := ephemeralIdentity.EDH(remoteEphemeralPublicKey) + + // We need to define an order for the result concatenation so that both sides derive the same key. + var result [96]byte + if outbound { + copy(result[0:32], l2e) + copy(result[32:64], e2l) + copy(result[64:96], e2e) + } else { + copy(result[0:32], e2l) + copy(result[32:64], l2e) + copy(result[64:96], e2e) + } + return sha3.Sum256(result[:]) +} diff --git a/primitives/identity.go b/primitives/identity.go new file mode 100644 index 0000000..6bf53d5 --- /dev/null +++ b/primitives/identity.go @@ -0,0 +1,53 @@ +package primitives + +import ( + "crypto/rand" + "git.openprivacy.ca/openprivacy/libricochet-go/utils" + "golang.org/x/crypto/ed25519" +) + +// Identity is an encapsulation of Name, PrivateKey and other features +// that make up a Tapir client. +// The purpose of Identity is to prevent other classes directly accessing private key +// and to ensure the integrity of security-critical functions. +type Identity struct { + Name string + edpk *ed25519.PrivateKey + edpubk *ed25519.PublicKey +} + +// Initialize is a courtesy function for initializing a V3 Identity in-code. +func Initialize(name string, pk *ed25519.PrivateKey, pubk *ed25519.PublicKey) Identity { + return Identity{name, pk, pubk} +} + +// InitializeEphemeral generates a new ephemeral identity, the private key of this identity is provided in the response. +func InitializeEphemeral() (Identity, ed25519.PrivateKey) { + epk, esk, _ := ed25519.GenerateKey(rand.Reader) + ephemeralPublicKey := ed25519.PublicKey(epk) + ephemeralPrivateKey := ed25519.PrivateKey(esk) + ephemeralIdentity := Initialize("", &ephemeralPrivateKey, &ephemeralPublicKey) + return ephemeralIdentity, ephemeralPrivateKey +} + +// PublicKeyBytes returns the public key associated with this Identity in serializable-friendly +// format. +func (i *Identity) PublicKeyBytes() []byte { + return *i.edpubk +} + +// PublicKey returns the public key associated with this Identity +func (i *Identity) PublicKey() ed25519.PublicKey { + return *i.edpubk +} + +// EDH performs a diffie helman operation on this identities private key with the given public key. +func (i *Identity) EDH(key ed25519.PublicKey) []byte { + secret := utils.EDH(*i.edpk, key) + return secret[:] +} + +// Hostname provides the onion address associated with this Identity. +func (i *Identity) Hostname() string { + return utils.GetTorV3Hostname(*i.edpubk) +} diff --git a/service.go b/service.go index b8d9af2..76815fd 100644 --- a/service.go +++ b/service.go @@ -2,9 +2,9 @@ package tapir import ( "crypto/rand" + "cwtch.im/tapir/primitives" "encoding/binary" "git.openprivacy.ca/openprivacy/libricochet-go/connectivity" - "git.openprivacy.ca/openprivacy/libricochet-go/identity" "git.openprivacy.ca/openprivacy/libricochet-go/log" "golang.org/x/crypto/ed25519" "golang.org/x/crypto/nacl/secretbox" @@ -15,7 +15,7 @@ import ( // Service defines the interface for a Tapir Service type Service interface { - Init(acn connectivity.ACN, privateKey ed25519.PrivateKey, identity identity.Identity) + Init(acn connectivity.ACN, privateKey ed25519.PrivateKey, identity *primitives.Identity) Connect(hostname string, application Application) (bool, error) Listen(application Application) error GetConnection(connectionID string) (Connection, error) @@ -27,7 +27,7 @@ type Service interface { type Connection interface { Hostname() string IsOutbound() bool - ID() *identity.Identity + ID() *primitives.Identity Expect() []byte SetHostname(hostname string) HasCapability(name string) bool @@ -46,19 +46,19 @@ type connection struct { encrypted bool key [32]byte App Application - identity *identity.Identity + identity *primitives.Identity outbound bool closed bool MaxLength int } // NewConnection creates a new Connection -func NewConnection(id identity.Identity, hostname string, outbound bool, conn net.Conn, app Application) Connection { +func NewConnection(id *primitives.Identity, hostname string, outbound bool, conn net.Conn, app Application) Connection { connection := new(connection) connection.hostname = hostname connection.conn = conn connection.App = app - connection.identity = &id + connection.identity = id connection.outbound = outbound connection.MaxLength = 1024 go connection.App.Init(connection) @@ -66,7 +66,7 @@ func NewConnection(id identity.Identity, hostname string, outbound bool, conn ne } // ID returns an identity.Identity encapsulation (for the purposes of cryptographic protocols) -func (c *connection) ID() *identity.Identity { +func (c *connection) ID() *primitives.Identity { return c.identity } diff --git a/cmd/main.go b/testing/tapir_integration_test.go similarity index 58% rename from cmd/main.go rename to testing/tapir_integration_test.go index 4becb78..db3e6df 100644 --- a/cmd/main.go +++ b/testing/tapir_integration_test.go @@ -1,16 +1,17 @@ -package main +package testing import ( - "crypto/rand" "cwtch.im/tapir" "cwtch.im/tapir/applications" "cwtch.im/tapir/networks/tor" + "cwtch.im/tapir/primitives" "git.openprivacy.ca/openprivacy/libricochet-go/connectivity" - "git.openprivacy.ca/openprivacy/libricochet-go/identity" "git.openprivacy.ca/openprivacy/libricochet-go/log" "git.openprivacy.ca/openprivacy/libricochet-go/utils" "golang.org/x/crypto/ed25519" - "os" + "runtime" + "sync" + "testing" "time" ) @@ -37,60 +38,82 @@ func (ea SimpleApp) Init(connection tapir.Connection) { } } +var AuthSuccess = false + // CheckConnection is a simple test that GetConnection is working. -func CheckConnection(service tapir.Service, hostname string) { +func CheckConnection(service tapir.Service, hostname string, group *sync.WaitGroup) { for { _, err := service.GetConnection(hostname) if err == nil { log.Infof("Authed!") + group.Done() return } - log.Errorf("Error %v", err) - time.Sleep(time.Second) + log.Infof("Waiting for Authentication...%v", err) + time.Sleep(time.Second * 5) } } -func main() { +func TestTapir(t *testing.T) { + numRoutinesStart := runtime.NumGoroutine() log.SetLevel(log.LevelDebug) - + log.Infof("Number of goroutines open at start: %d", runtime.NumGoroutine()) // Connect to Tor var acn connectivity.ACN acn, _ = connectivity.StartTor("./", "") acn.WaitTillBootstrapped() // Generate Server Keys - pubkey, privateKey, _ := ed25519.GenerateKey(rand.Reader) - sk := ed25519.PrivateKey(privateKey) - pk := ed25519.PublicKey(pubkey) - id := identity.InitializeV3("server", &sk, &pk) - - // Init a Client to Connect to the Server - client, clienthostname := genclient(acn) - go connectclient(client, pubkey) + id, sk := primitives.InitializeEphemeral() // Init the Server running the Simple App. var service tapir.Service service = new(tor.BaseOnionService) - service.Init(acn, sk, id) - go CheckConnection(service, clienthostname) - service.Listen(SimpleApp{}) + service.Init(acn, sk, &id) + + // Goroutine Management + sg := new(sync.WaitGroup) + sg.Add(1) + go func() { + service.Listen(SimpleApp{}) + sg.Done() + }() + + // Wait for server to come online + time.Sleep(time.Second * 30) + wg := new(sync.WaitGroup) + wg.Add(2) + // Init a Client to Connect to the Server + client, clienthostname := genclient(acn) + go connectclient(client, id.PublicKey(), wg) + CheckConnection(service, clienthostname, wg) + wg.Wait() + // Wait for Server to Sync + time.Sleep(time.Second * 2) + log.Infof("Closing ACN...") + acn.Close() + sg.Wait() + time.Sleep(time.Second * 2) + log.Infof("Number of goroutines open at close: %d", runtime.NumGoroutine()) + if numRoutinesStart != runtime.NumGoroutine() { + t.Errorf("Potential goroutine leak: Num Start:%v NumEnd: %v", numRoutinesStart, runtime.NumGoroutine()) + } + if !AuthSuccess { + t.Fatalf("Integration Test FAILED, client did not auth with server") + } } func genclient(acn connectivity.ACN) (tapir.Service, string) { - pubkey, privateKey, _ := ed25519.GenerateKey(rand.Reader) - sk := ed25519.PrivateKey(privateKey) - pk := ed25519.PublicKey(pubkey) - id := identity.InitializeV3("client", &sk, &pk) + id, sk := primitives.InitializeEphemeral() var client tapir.Service client = new(tor.BaseOnionService) - client.Init(acn, sk, id) - return client, utils.GetTorV3Hostname(pk) + client.Init(acn, sk, &id) + return client, id.Hostname() } // Client will Connect and launch it's own Echo App goroutine. -func connectclient(client tapir.Service, key ed25519.PublicKey) { - +func connectclient(client tapir.Service, key ed25519.PublicKey, group *sync.WaitGroup) { client.Connect(utils.GetTorV3Hostname(key), SimpleApp{}) // Once connected, it shouldn't take long to authenticate and run the application. So for the purposes of this demo @@ -99,6 +122,6 @@ func connectclient(client tapir.Service, key ed25519.PublicKey) { conn, _ := client.GetConnection(utils.GetTorV3Hostname(key)) log.Debugf("Client has Auth: %v", conn.HasCapability(applications.AuthCapability)) - - os.Exit(0) + AuthSuccess = true + group.Done() }