From b9d73b83f7b9d68ff6a9d5f3b7d228e931b7d020 Mon Sep 17 00:00:00 2001 From: josh <josh@elixxir.io> Date: Fri, 6 May 2022 16:19:38 -0700 Subject: [PATCH] Add logic for authenticated connections --- catalog/messageTypes.go | 5 + connections/authenticated/authenticated.go | 180 ++++++++++++++++++ connections/authenticated/authenticated.pb.go | 90 +++++++++ connections/authenticated/authenticated.proto | 21 ++ connections/authenticated/client.go | 127 ++++++++++++ connections/authenticated/generateProto.sh | 3 + connections/authenticated/server.go | 153 +++++++++++++++ 7 files changed, 579 insertions(+) create mode 100644 connections/authenticated/authenticated.go create mode 100644 connections/authenticated/authenticated.pb.go create mode 100644 connections/authenticated/authenticated.proto create mode 100644 connections/authenticated/client.go create mode 100755 connections/authenticated/generateProto.sh create mode 100644 connections/authenticated/server.go diff --git a/catalog/messageTypes.go b/catalog/messageTypes.go index 969e6c703..c6107e056 100644 --- a/catalog/messageTypes.go +++ b/catalog/messageTypes.go @@ -42,4 +42,9 @@ const ( // EndFileTransfer is sent once all file parts have been transmitted to // inform the receiver that the file transfer has ended. EndFileTransfer = 51 + + // IdentityAuthenticationRequest is sent by the recipient + // of an authenticated connection request + // (see the connections/ package) + ConnectionAuthenticationRequest = 60 ) diff --git a/connections/authenticated/authenticated.go b/connections/authenticated/authenticated.go new file mode 100644 index 000000000..ea4e9ef2e --- /dev/null +++ b/connections/authenticated/authenticated.go @@ -0,0 +1,180 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package authenticated + +import ( + "github.com/pkg/errors" + jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/auth" + "gitlab.com/elixxir/client/catalog" + "gitlab.com/elixxir/client/cmix" + "gitlab.com/elixxir/client/connections/connect" + clientE2e "gitlab.com/elixxir/client/e2e" + "gitlab.com/elixxir/client/e2e/ratchet/partner" + "gitlab.com/elixxir/client/storage/versioned" + "gitlab.com/elixxir/crypto/contact" + "gitlab.com/elixxir/crypto/cyclic" + "gitlab.com/elixxir/crypto/fastRNG" + "gitlab.com/elixxir/ekv" + "gitlab.com/xx_network/crypto/signature/rsa" + "gitlab.com/xx_network/primitives/id" + "sync" + "time" +) + +// Connection is a connect.Connection interface that +// has the receiver authenticating their identity back to the +// initiator. +type Connection interface { + // Connection is the base connections API. This allows + // sending and listening to the partner + connect.Connection + + // IsAuthenticated is a function which returns whether the + // authenticated connection has been completely established. + IsAuthenticated() bool +} + +// ConnectionCallback is the callback format required to retrieve +// new authenticated.Connection objects as they are established. +type ConnectionCallback func(connection Connection) + +// ConnectWithAuthentication is called by the client, ie the initiator +// of an authenticated.Connection. This will send a request for an +// authenticated connection to the server. +func ConnectWithAuthentication(recipient contact.Contact, + myId *id.ID, dhPrivKey *cyclic.Int, + rng *fastRNG.StreamGenerator, grp *cyclic.Group, net cmix.Client, + p connect.Params) (Connection, error) { + + // Build an ephemeral KV + kv := versioned.NewKV(ekv.MakeMemstore()) + + // Build E2e handler + err := clientE2e.Init(kv, myId, dhPrivKey, grp, p.Rekey) + if err != nil { + return nil, err + } + e2eHandler, err := clientE2e.Load(kv, net, myId, grp, rng, p.Event) + if err != nil { + return nil, err + } + + // Build the callback for the connection being established + authConnChan := make(chan Connection, 1) + cb := func(authConn Connection) { + authConnChan <- authConn + } + + // Build callback for E2E negotiation in the auth package + clientHandler := getClient(cb, e2eHandler, p) + + // Register a listener for the server's response + e2eHandler.RegisterListener(recipient.ID, + catalog.ConnectionAuthenticationRequest, + clientHandler) + + // Build auth object for E2E negotiation + authState, err := auth.NewState(kv, net, e2eHandler, + rng, p.Event, p.Auth, clientHandler, nil) + if err != nil { + return nil, err + } + + // Perform the auth request + _, err = authState.Request(recipient, nil) + if err != nil { + return nil, err + } + + // Block waiting for auth to confirm + jww.DEBUG.Printf("Connection waiting for authenticated "+ + "connection with %s to be established...", recipient.ID.String()) + + timeout := time.NewTimer(p.Timeout) + select { + case newConnection := <-authConnChan: + if newConnection == nil { + return nil, errors.Errorf( + "Unable to complete authenticated connection with partner %s", + recipient.ID.String()) + } + + return newConnection, nil + case <-timeout.C: + return nil, errors.Errorf("Authenticated connection with "+ + "partner %s timed out", recipient.ID.String()) + } +} + +// StartAuthenticatedConnectionServer is Called by the receiver of an +// authenticated connection request. Calling this indicated that they will +// recognize and respond to identity authentication requests by +// a client. +func StartAuthenticatedConnectionServer(cb ConnectionCallback, + myId *id.ID, salt []byte, rsaPrivkey *rsa.PrivateKey, privKey *cyclic.Int, + rng *fastRNG.StreamGenerator, grp *cyclic.Group, net cmix.Client, + p connect.Params) error { + + // Build an ephemeral KV + kv := versioned.NewKV(ekv.MakeMemstore()) + + // Build E2e handler + err := clientE2e.Init(kv, myId, privKey, grp, p.Rekey) + if err != nil { + return err + } + e2eHandler, err := clientE2e.Load(kv, net, myId, grp, rng, p.Event) + if err != nil { + return err + } + + // Build callback for E2E negotiation + callback := getServer(cb, e2eHandler, net, rsaPrivkey, rng, p) + + // Build auth object for E2E negotiation + _, err = auth.NewState(kv, net, e2eHandler, + rng, p.Event, p.Auth, callback, nil) + + return err +} + +// handler provides an implementation for the authenticated.Connection +// interface. +type handler struct { + connect.Connection + isAuthenticated bool + authMux sync.Mutex +} + +// buildAuthenticatedConnection assembles an authenticated.Connection object. +// This is called by the connection server once it has sent an +// IdentityAuthentication to the client. +// This is called by the client when they have received and confirmed the data within +// a IdentityAuthentication message. +func buildAuthenticatedConnection(partner partner.Manager, + e2eHandler clientE2e.Handler, + p connect.Params) *handler { + + return &handler{ + Connection: connect.BuildConnection(partner, e2eHandler, p), + } +} + +// IsAuthenticated returns whether the Connection has completed the authentication +// process. +func (h *handler) IsAuthenticated() bool { + return h.isAuthenticated +} + +// setAuthenticated is a helper function which sets the Connection as authenticated. +func (h *handler) setAuthenticated() { + h.authMux.Lock() + defer h.authMux.Unlock() + h.isAuthenticated = true +} diff --git a/connections/authenticated/authenticated.pb.go b/connections/authenticated/authenticated.pb.go new file mode 100644 index 000000000..86716c0e6 --- /dev/null +++ b/connections/authenticated/authenticated.pb.go @@ -0,0 +1,90 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: connections/authenticated/authenticated.proto + +package authenticated // import "gitlab.com/elixxir/client/connections/authenticated" + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package + +// Sent by the receiver of the authenticated connection request. +type IdentityAuthentication struct { + Signature []byte `protobuf:"bytes,1,opt,name=Signature,proto3" json:"Signature,omitempty"` + // established between the two partners + RsaPubKey []byte `protobuf:"bytes,2,opt,name=RsaPubKey,proto3" json:"RsaPubKey,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *IdentityAuthentication) Reset() { *m = IdentityAuthentication{} } +func (m *IdentityAuthentication) String() string { return proto.CompactTextString(m) } +func (*IdentityAuthentication) ProtoMessage() {} +func (*IdentityAuthentication) Descriptor() ([]byte, []int) { + return fileDescriptor_authenticated_3bb97abbbfff0896, []int{0} +} +func (m *IdentityAuthentication) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_IdentityAuthentication.Unmarshal(m, b) +} +func (m *IdentityAuthentication) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_IdentityAuthentication.Marshal(b, m, deterministic) +} +func (dst *IdentityAuthentication) XXX_Merge(src proto.Message) { + xxx_messageInfo_IdentityAuthentication.Merge(dst, src) +} +func (m *IdentityAuthentication) XXX_Size() int { + return xxx_messageInfo_IdentityAuthentication.Size(m) +} +func (m *IdentityAuthentication) XXX_DiscardUnknown() { + xxx_messageInfo_IdentityAuthentication.DiscardUnknown(m) +} + +var xxx_messageInfo_IdentityAuthentication proto.InternalMessageInfo + +func (m *IdentityAuthentication) GetSignature() []byte { + if m != nil { + return m.Signature + } + return nil +} + +func (m *IdentityAuthentication) GetRsaPubKey() []byte { + if m != nil { + return m.RsaPubKey + } + return nil +} + +func init() { + proto.RegisterType((*IdentityAuthentication)(nil), "authenticatedConnectionMessages.IdentityAuthentication") +} + +func init() { + proto.RegisterFile("connections/authenticated/authenticated.proto", fileDescriptor_authenticated_3bb97abbbfff0896) +} + +var fileDescriptor_authenticated_3bb97abbbfff0896 = []byte{ + // 166 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xd2, 0x4d, 0xce, 0xcf, 0xcb, + 0x4b, 0x4d, 0x2e, 0xc9, 0xcc, 0xcf, 0x2b, 0xd6, 0x4f, 0x2c, 0x2d, 0xc9, 0x48, 0xcd, 0x2b, 0xc9, + 0x4c, 0x4e, 0x2c, 0x49, 0x4d, 0x41, 0xe5, 0xe9, 0x15, 0x14, 0xe5, 0x97, 0xe4, 0x0b, 0xc9, 0xa3, + 0x08, 0x3a, 0xc3, 0xf5, 0xfa, 0xa6, 0x16, 0x17, 0x27, 0xa6, 0xa7, 0x16, 0x2b, 0x85, 0x70, 0x89, + 0x79, 0xa6, 0x80, 0x14, 0x94, 0x54, 0x3a, 0x22, 0x94, 0x66, 0xe6, 0xe7, 0x09, 0xc9, 0x70, 0x71, + 0x06, 0x67, 0xa6, 0xe7, 0x25, 0x96, 0x94, 0x16, 0xa5, 0x4a, 0x30, 0x2a, 0x30, 0x6a, 0xf0, 0x04, + 0x21, 0x04, 0x40, 0xb2, 0x41, 0xc5, 0x89, 0x01, 0xa5, 0x49, 0xde, 0xa9, 0x95, 0x12, 0x4c, 0x10, + 0x59, 0xb8, 0x80, 0x93, 0x69, 0x94, 0x71, 0x7a, 0x66, 0x49, 0x4e, 0x62, 0x92, 0x5e, 0x72, 0x7e, + 0xae, 0x7e, 0x6a, 0x4e, 0x66, 0x45, 0x45, 0x66, 0x91, 0x7e, 0x72, 0x4e, 0x66, 0x6a, 0x5e, 0x89, + 0x3e, 0x4e, 0x1f, 0x24, 0xb1, 0x81, 0x1d, 0x6d, 0x0c, 0x08, 0x00, 0x00, 0xff, 0xff, 0x7c, 0xda, + 0xe4, 0xd4, 0xe5, 0x00, 0x00, 0x00, +} diff --git a/connections/authenticated/authenticated.proto b/connections/authenticated/authenticated.proto new file mode 100644 index 000000000..492bad5de --- /dev/null +++ b/connections/authenticated/authenticated.proto @@ -0,0 +1,21 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +syntax = "proto3"; +package authenticatedConnectionMessages; +option go_package = "gitlab.com/elixxir/client/connections/authenticated"; + +// Sent by the receiver of the authenticated connection request. +message IdentityAuthentication { + bytes Signature = 1; // Signature of the connection fingerprint + // established between the two partners + bytes RsaPubKey = 2; // The RSA public key of the sender of this message, + // PEM-encoded +} + + + diff --git a/connections/authenticated/client.go b/connections/authenticated/client.go new file mode 100644 index 000000000..e1d137f2e --- /dev/null +++ b/connections/authenticated/client.go @@ -0,0 +1,127 @@ +package authenticated + +import ( + "github.com/golang/protobuf/proto" + jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/auth" + "gitlab.com/elixxir/client/cmix/identity/receptionID" + "gitlab.com/elixxir/client/cmix/rounds" + "gitlab.com/elixxir/client/connections/connect" + clientE2e "gitlab.com/elixxir/client/e2e" + "gitlab.com/elixxir/client/e2e/receive" + "gitlab.com/elixxir/crypto/contact" + "gitlab.com/xx_network/crypto/signature/rsa" +) + +// clientListenerName is the name of the client's listener interface. +const clientListenerName = "AuthenticatedClientListener" + +// client is an interface that collates the receive.Listener and +// auth.Callbacks interfaces. This allows a singular object +// to handle auth package endpoints as well as handling the +// custom catalog.MessageType's. +type client interface { + receive.Listener + auth.Callbacks +} + +// clientHandler provides an implementation of the client interface. +type clientHandler struct { + // connectionCallback allows an authenticated.Connection + // to be passed back upon establishment. + connectionCallback ConnectionCallback + + // Used for building new Connection objects + connectionE2e clientE2e.Handler + connectionParams connect.Params +} + +// getClient returns a client interface to be used to handle +// auth.Callbacks and receive.Listener operations. +func getClient(cb ConnectionCallback, + e2e clientE2e.Handler, p connect.Params) client { + return clientHandler{ + connectionCallback: cb, + connectionE2e: e2e, + connectionParams: p, + } +} + +// Request will be called when an auth Request message is processed. +func (a clientHandler) Request(requestor contact.Contact, + receptionID receptionID.EphemeralIdentity, round rounds.Round) { +} + +// Confirm will be called when an auth Confirm message is processed. +func (a clientHandler) Confirm(requestor contact.Contact, + receptionID receptionID.EphemeralIdentity, round rounds.Round) { +} + +// Reset will be called when an auth Reset operation occurs. +func (a clientHandler) Reset(requestor contact.Contact, + receptionID receptionID.EphemeralIdentity, round rounds.Round) { +} + +// Hear handles the reception of an IdentityAuthentication by the +// server. +func (a clientHandler) Hear(item receive.Message) { + // Process the message data into a protobuf + iar := &IdentityAuthentication{} + err := proto.Unmarshal(item.Payload, iar) + if err != nil { + jww.ERROR.Printf("Unable to build connection with "+ + "partner %s: %+v", item.Sender, err) + // Send a nil connection to avoid hold-ups down the line + a.connectionCallback(nil) + return + } + + // Process the PEM encoded public key to an rsa.PublicKey object + partnerPubKey, err := rsa.LoadPublicKeyFromPem(iar.RsaPubKey) + if err != nil { + jww.ERROR.Printf("Unable to build connection with "+ + "partner %s: %+v", item.Sender, err) + // Send a nil connection to avoid hold-ups down the line + a.connectionCallback(nil) + return + } + + // Get the new partner + newPartner, err := a.connectionE2e.GetPartner(item.Sender) + if err != nil { + jww.ERROR.Printf("Unable to build connection with "+ + "partner %s: %+v", item.Sender, err) + // Send a nil connection to avoid hold-ups down the line + a.connectionCallback(nil) + return + } + + // The connection fingerprint (hashed) represents a shared nonce + // between these two partners + conneptionFp := newPartner.ConnectionFingerprint().Bytes() + + // Hash the connection fingerprint + opts := rsa.NewDefaultOptions() + h := opts.Hash.New() + h.Write(conneptionFp) + nonce := h.Sum(nil) + + // Verify the signature + err = rsa.Verify(partnerPubKey, opts.Hash, nonce, iar.Signature, opts) + if err != nil { + jww.ERROR.Printf("Unable to build connection with "+ + "partner %s: %+v", item.Sender, err) + // Send a nil connection to avoid hold-ups down the line + a.connectionCallback(nil) + } + + // If successful, pass along the established connection + jww.DEBUG.Printf("Connection auth request for %s confirmed", + item.Sender.String()) + a.connectionCallback(buildAuthenticatedConnection(newPartner, a.connectionE2e, + a.connectionParams)) +} + +func (a clientHandler) Name() string { + return clientListenerName +} diff --git a/connections/authenticated/generateProto.sh b/connections/authenticated/generateProto.sh new file mode 100755 index 000000000..43c03831f --- /dev/null +++ b/connections/authenticated/generateProto.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +protoc --go_out=paths=source_relative:. connections/authenticated/authenticated.proto diff --git a/connections/authenticated/server.go b/connections/authenticated/server.go new file mode 100644 index 000000000..65c3b5eb8 --- /dev/null +++ b/connections/authenticated/server.go @@ -0,0 +1,153 @@ +package authenticated + +import ( + "github.com/golang/protobuf/proto" + jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/catalog" + "gitlab.com/elixxir/client/cmix" + "gitlab.com/elixxir/client/cmix/identity/receptionID" + "gitlab.com/elixxir/client/cmix/rounds" + "gitlab.com/elixxir/client/connections/connect" + clientE2e "gitlab.com/elixxir/client/e2e" + "gitlab.com/elixxir/crypto/contact" + "gitlab.com/elixxir/crypto/fastRNG" + "gitlab.com/xx_network/crypto/signature/rsa" + "gitlab.com/xx_network/primitives/id" +) + +// serverHandler handles the io operations of an +// authenticated.Connection server. +type serverHandler struct { + // connectionCallback allows an authenticated.Connection + // to be passed back upon establishment. + connectionCallback ConnectionCallback + + // Used for building new Connection objects + connectionE2e clientE2e.Handler + connectionParams connect.Params + + // Used for tracking the round the identity confirmation message was sent. + // A successful round assumes the client received the confirmation and + // an authenticated.Connection has been established + services cmix.Client + + // Used for signing the connection fingerprint used as a nonce + // to confirm the user's identity + privateKey *rsa.PrivateKey + rng *fastRNG.StreamGenerator +} + +// getServer returns a serverHandler object. This is used to pass +// into the auth.State object to handle the auth.Callbacks. +func getServer(cb ConnectionCallback, + e2e clientE2e.Handler, services cmix.Client, + privateKey *rsa.PrivateKey, + rng *fastRNG.StreamGenerator, p connect.Params) serverHandler { + return serverHandler{ + connectionCallback: cb, + connectionE2e: e2e, + connectionParams: p, + privateKey: privateKey, + rng: rng, + services: services, + } +} + +// Request will be called when an auth Request message is processed. +func (a serverHandler) Request(requestor contact.Contact, + receptionID receptionID.EphemeralIdentity, round rounds.Round) { + + // After confirmation, get the new partner + newPartner, err := a.connectionE2e.GetPartner(requestor.ID) + if err != nil { + jww.ERROR.Printf("Unable to build connection with "+ + "partner %s: %+v", requestor.ID, err) + // Send a nil connection to avoid hold-ups down the line + a.connectionCallback(nil) + return + } + + authConn := buildAuthenticatedConnection(newPartner, a.connectionE2e, + a.connectionParams) + + // The connection fingerprint (hashed) represents a shared nonce + // between these two partners + connectionFp := newPartner.ConnectionFingerprint().Bytes() + + opts := rsa.NewDefaultOptions() + h := opts.Hash.New() + h.Write(connectionFp) + nonce := h.Sum(nil) + + // Sign the connection fingerprint + stream := a.rng.GetStream() + defer stream.Close() + signature, err := rsa.Sign(stream, a.privateKey, + opts.Hash, nonce, opts) + if err != nil { + jww.ERROR.Printf("Unable to build connection with "+ + "partner %s: %+v", requestor.ID, err) + // Send a nil connection to avoid hold-ups down the line + a.connectionCallback(nil) + } + + // Construct message + pemEncodedRsaPubKey := rsa.CreatePublicKeyPem(a.privateKey.GetPublic()) + iar := &IdentityAuthentication{ + Signature: signature, + RsaPubKey: pemEncodedRsaPubKey, + } + payload, err := proto.Marshal(iar) + if err != nil { + jww.ERROR.Printf("Unable to build connection with "+ + "partner %s: %+v", requestor.ID, err) + // Send a nil connection to avoid hold-ups down the line + a.connectionCallback(nil) + } + + // Send message to user + rids, _, _, err := authConn.SendE2E(catalog.ConnectionAuthenticationRequest, + payload, clientE2e.GetDefaultParams()) + if err != nil { + jww.ERROR.Printf("Unable to build connection with "+ + "partner %s: %+v", requestor.ID, err) + // Send a nil connection to avoid hold-ups down the line + a.connectionCallback(nil) + } + + // Determine that the message is properly sent by tracking the success + // of the round(s) + roundCb := cmix.RoundEventCallback(func(allRoundsSucceeded, + timedOut bool, rounds map[id.Round]cmix.RoundResult) { + if allRoundsSucceeded { + // If rounds succeeded, assume recipient has successfully + // confirmed the authentication + authConn.setAuthenticated() + a.connectionCallback(authConn) + } else { + jww.ERROR.Printf("Unable to build connection with "+ + "partner %s: %+v", requestor.ID, err) + // Send a nil connection to avoid hold-ups down the line + a.connectionCallback(nil) + } + }) + err = a.services.GetRoundResults(a.connectionParams.Timeout, + roundCb, rids...) + if err != nil { + jww.ERROR.Printf("Unable to build connection with "+ + "partner %s: %+v", requestor.ID, err) + // Send a nil connection to avoid hold-ups down the line + a.connectionCallback(nil) + } + +} + +// Confirm will be called when an auth Confirm message is processed. +func (a serverHandler) Confirm(requestor contact.Contact, + receptionID receptionID.EphemeralIdentity, round rounds.Round) { +} + +// Reset will be called when an auth Reset operation occurs. +func (a serverHandler) Reset(requestor contact.Contact, + receptionID receptionID.EphemeralIdentity, round rounds.Round) { +} -- GitLab