diff --git a/application/acceptallcontactmanager.go b/application/acceptallcontactmanager.go index 8bc34a8..cb136a3 100644 --- a/application/acceptallcontactmanager.go +++ b/application/acceptallcontactmanager.go @@ -2,6 +2,7 @@ package application import ( "crypto/rsa" + "golang.org/x/crypto/ed25519" ) // AcceptAllContactManager implements the contact manager interface an presumes @@ -14,6 +15,11 @@ func (aacm *AcceptAllContactManager) LookupContact(hostname string, publicKey rs return true, true } +// LookupContact returns that a contact is known and allowed to communicate for all cases. +func (aacm *AcceptAllContactManager) LookupContactV3(hostname string, publicKey ed25519.PublicKey) (allowed, known bool) { + return true, true +} + func (aacm *AcceptAllContactManager) ContactRequest(name string, message string) string { return "Accepted" } diff --git a/application/application.go b/application/application.go index dea08cc..e3d0b20 100644 --- a/application/application.go +++ b/application/application.go @@ -16,6 +16,7 @@ import ( type RicochetApplication struct { contactManager ContactManagerInterface privateKey *rsa.PrivateKey + v3identity identity.Identity name string l net.Listener instances []*ApplicationInstance @@ -30,6 +31,13 @@ func (ra *RicochetApplication) Init(name string, pk *rsa.PrivateKey, af Applicat ra.contactManager = cm } +func (ra *RicochetApplication) InitV3(name string, v3identity identity.Identity, af ApplicationInstanceFactory, cm ContactManagerInterface) { + ra.name = name + ra.v3identity = v3identity + ra.aif = af + ra.contactManager = cm +} + // TODO: Reimplement OnJoin, OnLeave Events. func (ra *RicochetApplication) handleConnection(conn net.Conn) { rc, err := goricochet.NegotiateVersionInbound(conn) @@ -41,12 +49,18 @@ func (ra *RicochetApplication) handleConnection(conn net.Conn) { ich := connection.HandleInboundConnection(rc) - err = ich.ProcessAuthAsServer(identity.Initialize(ra.name, ra.privateKey), ra.contactManager.LookupContact) + if ra.v3identity.Initialized() { + err = ich.ProcessAuthAsV3Server(ra.v3identity, ra.contactManager.LookupContactV3) + } else { + err = ich.ProcessAuthAsServer(identity.Initialize(ra.name, ra.privateKey), ra.contactManager.LookupContact) + } + if err != nil { - log.Printf("There was an error") + log.Printf("There was an error authenticating the connection: %v", err) conn.Close() return } + rc.TraceLog(true) rai := ra.aif.GetApplicationInstance(rc) ra.lock.Lock() @@ -81,7 +95,19 @@ func (ra *RicochetApplication) Open(onionAddress string, requestMessage string) return nil, err } - known, err := connection.HandleOutboundConnection(rc).ProcessAuthAsClient(identity.Initialize(ra.name, ra.privateKey)) + och := connection.HandleOutboundConnection(rc) + + var known bool + if ra.v3identity.Initialized() { + known, err = och.ProcessAuthAsV3Client(ra.v3identity) + } else { + known, err = och.ProcessAuthAsClient(identity.Initialize(ra.name, ra.privateKey)) + } + + if err != nil { + log.Printf("There was an error authenticating the connection: %v", err) + return nil, err + } rai := ra.aif.GetApplicationInstance(rc) go rc.Process(rai) @@ -99,7 +125,6 @@ func (ra *RicochetApplication) Open(onionAddress string, requestMessage string) log.Printf("could not contact %s", err) } } - ra.HandleApplicationInstance(rai) return rai, nil } @@ -126,7 +151,7 @@ func (ra *RicochetApplication) ConnectionCount() int { } func (ra *RicochetApplication) Run(l net.Listener) { - if ra.privateKey == nil || ra.contactManager == nil { + if (ra.privateKey == nil && !ra.v3identity.Initialized()) || ra.contactManager == nil { return } ra.l = l diff --git a/application/contactmanagerinterface.go b/application/contactmanagerinterface.go index 4573178..eaa92fc 100644 --- a/application/contactmanagerinterface.go +++ b/application/contactmanagerinterface.go @@ -2,10 +2,12 @@ package application import ( "crypto/rsa" + "golang.org/x/crypto/ed25519" ) // ContactManagerInterface provides a mechanism for autonous applications // to make decisions on what connections to accept or reject. type ContactManagerInterface interface { LookupContact(hostname string, publicKey rsa.PublicKey) (allowed, known bool) + LookupContactV3(hostname string, publicKey ed25519.PublicKey) (allowed, known bool) } diff --git a/application/examples/v3/main.go b/application/examples/v3/main.go new file mode 100644 index 0000000..0dac8cd --- /dev/null +++ b/application/examples/v3/main.go @@ -0,0 +1,22 @@ +package main + +import ( + "crypto/rand" + "encoding/base32" + "git.openprivacy.ca/openprivacy/libricochet-go/application" + "git.openprivacy.ca/openprivacy/libricochet-go/utils" + "golang.org/x/crypto/ed25519" + "log" + "strings" +) + +// An example of how to setup a v3 onion service in go +func main() { + cpubk, cprivk, _ := ed25519.GenerateKey(rand.Reader) + l, err := application.SetupOnionV3("127.0.0.1:9051", "tcp4", "", cprivk, 9878) + utils.CheckError(err) + log.Printf("Got Listener %v", l.Addr().String()) + decodedPub, err := base32.StdEncoding.DecodeString(strings.ToUpper(l.Addr().String()[:56])) + log.Printf("Decoded Public Key: %x %v", decodedPub[:32], err) + log.Printf("ed25519 Public Key: %x", cpubk) +} diff --git a/application/ricochetonion.go b/application/ricochetonion.go index 82f965d..6c73b3f 100644 --- a/application/ricochetonion.go +++ b/application/ricochetonion.go @@ -2,7 +2,10 @@ package application import ( "crypto/rsa" + "crypto/sha512" + "encoding/base64" "github.com/yawning/bulb" + "golang.org/x/crypto/ed25519" "net" ) @@ -25,3 +28,35 @@ func SetupOnion(torControlAddress string, torControlSocketType string, authentic return c.NewListener(cfg, onionport) } + +func SetupOnionV3(torControlAddress string, torControlSocketType string, authentication string, pk ed25519.PrivateKey, onionport uint16) (net.Listener, error) { + c, err := bulb.Dial(torControlSocketType, torControlAddress) + if err != nil { + return nil, err + } + + if err := c.Authenticate(authentication); err != nil { + return nil, err + } + + digest := sha512.Sum512(pk[:32]) + digest[0] &= 248 + digest[31] &= 127 + digest[31] |= 64 + + var privkey [64]byte + copy(privkey[0:32], digest[:32]) + copy(privkey[32:64], digest[32:]) + + onionPK := &bulb.OnionPrivateKey{ + KeyType: "ED25519-V3", + Key: base64.StdEncoding.EncodeToString(privkey[0:64]), + } + + cfg := &bulb.NewOnionConfig{ + DiscardPK: true, + PrivateKey: onionPK, + } + + return c.NewListener(cfg, onionport) +} diff --git a/channels/chatchannel.go b/channels/chatchannel.go index 767e43b..42c1f24 100644 --- a/channels/chatchannel.go +++ b/channels/chatchannel.go @@ -1,10 +1,10 @@ package channels import ( - "github.com/golang/protobuf/proto" "git.openprivacy.ca/openprivacy/libricochet-go/utils" "git.openprivacy.ca/openprivacy/libricochet-go/wire/chat" "git.openprivacy.ca/openprivacy/libricochet-go/wire/control" + "github.com/golang/protobuf/proto" "time" ) diff --git a/channels/chatchannel_test.go b/channels/chatchannel_test.go index 64ab187..9709892 100644 --- a/channels/chatchannel_test.go +++ b/channels/chatchannel_test.go @@ -1,10 +1,10 @@ package channels import ( - "github.com/golang/protobuf/proto" "git.openprivacy.ca/openprivacy/libricochet-go/utils" "git.openprivacy.ca/openprivacy/libricochet-go/wire/chat" "git.openprivacy.ca/openprivacy/libricochet-go/wire/control" + "github.com/golang/protobuf/proto" "testing" "time" ) diff --git a/channels/contactrequestchannel.go b/channels/contactrequestchannel.go index d2a3f66..a195ffc 100644 --- a/channels/contactrequestchannel.go +++ b/channels/contactrequestchannel.go @@ -1,10 +1,10 @@ package channels import ( - "github.com/golang/protobuf/proto" "git.openprivacy.ca/openprivacy/libricochet-go/utils" "git.openprivacy.ca/openprivacy/libricochet-go/wire/contact" "git.openprivacy.ca/openprivacy/libricochet-go/wire/control" + "github.com/golang/protobuf/proto" ) // Defining Versions diff --git a/channels/contactrequestchannel_test.go b/channels/contactrequestchannel_test.go index 6af05ff..05332d2 100644 --- a/channels/contactrequestchannel_test.go +++ b/channels/contactrequestchannel_test.go @@ -1,10 +1,10 @@ package channels import ( - "github.com/golang/protobuf/proto" "git.openprivacy.ca/openprivacy/libricochet-go/utils" "git.openprivacy.ca/openprivacy/libricochet-go/wire/contact" "git.openprivacy.ca/openprivacy/libricochet-go/wire/control" + "github.com/golang/protobuf/proto" "testing" ) diff --git a/channels/hiddenserviceauthchannel.go b/channels/hiddenserviceauthchannel.go index 7cb74ab..82f6185 100644 --- a/channels/hiddenserviceauthchannel.go +++ b/channels/hiddenserviceauthchannel.go @@ -7,11 +7,11 @@ import ( "crypto/rsa" "crypto/sha256" "encoding/asn1" - "github.com/golang/protobuf/proto" "git.openprivacy.ca/openprivacy/libricochet-go/identity" "git.openprivacy.ca/openprivacy/libricochet-go/utils" "git.openprivacy.ca/openprivacy/libricochet-go/wire/auth" "git.openprivacy.ca/openprivacy/libricochet-go/wire/control" + "github.com/golang/protobuf/proto" "io" ) diff --git a/channels/hiddenserviceauthchannel_test.go b/channels/hiddenserviceauthchannel_test.go index c75c67b..028d21d 100644 --- a/channels/hiddenserviceauthchannel_test.go +++ b/channels/hiddenserviceauthchannel_test.go @@ -3,10 +3,10 @@ package channels import ( "bytes" "crypto/rsa" - "github.com/golang/protobuf/proto" "git.openprivacy.ca/openprivacy/libricochet-go/identity" "git.openprivacy.ca/openprivacy/libricochet-go/utils" "git.openprivacy.ca/openprivacy/libricochet-go/wire/control" + "github.com/golang/protobuf/proto" "testing" ) diff --git a/channels/v3/inbound/3dhauthchannel.go b/channels/v3/inbound/3dhauthchannel.go new file mode 100644 index 0000000..49e0bc8 --- /dev/null +++ b/channels/v3/inbound/3dhauthchannel.go @@ -0,0 +1,172 @@ +package inbound + +import ( + "crypto/rand" + "errors" + "git.openprivacy.ca/openprivacy/libricochet-go/channels" + "git.openprivacy.ca/openprivacy/libricochet-go/identity" + "git.openprivacy.ca/openprivacy/libricochet-go/utils" + "git.openprivacy.ca/openprivacy/libricochet-go/wire/auth/3edh" + "git.openprivacy.ca/openprivacy/libricochet-go/wire/control" + "github.com/golang/protobuf/proto" + "golang.org/x/crypto/ed25519" + "golang.org/x/crypto/nacl/secretbox" + "golang.org/x/crypto/pbkdf2" + "golang.org/x/crypto/sha3" + "log" +) + +// Server3DHAuthChannel wraps implementation of im.ricochet.auth.hidden-service" +type Server3DHAuthChannel struct { + // PrivateKey must be set for client-side authentication channels + ServerIdentity identity.Identity + + // Callbacks + ServerAuthValid func(hostname string, publicKey ed25519.PublicKey) (allowed, known bool) + ServerAuthInvalid func(err error) + + // Internal state + clientPubKey, clientEphmeralPublicKey, serverEphemeralPublicKey ed25519.PublicKey + serverEphemeralPrivateKey ed25519.PrivateKey + channel *channels.Channel +} + +// Type returns the type string for this channel, e.g. "im.ricochet.chat". +func (ah *Server3DHAuthChannel) Type() string { + return "im.ricochet.auth.3dh" +} + +// Singleton Returns whether or not the given channel type is a singleton +func (ah *Server3DHAuthChannel) Singleton() bool { + return true +} + +// OnlyClientCanOpen ... +func (ah *Server3DHAuthChannel) OnlyClientCanOpen() bool { + return true +} + +// Bidirectional Returns whether or not the given channel allows anyone to send messages +func (ah *Server3DHAuthChannel) Bidirectional() bool { + return false +} + +// RequiresAuthentication Returns whether or not the given channel type requires authentication +func (ah *Server3DHAuthChannel) RequiresAuthentication() string { + return "none" +} + +// Closed is called when the channel is closed for any reason. +func (ah *Server3DHAuthChannel) Closed(err error) { + +} + +// OpenInbound is the first method called for an inbound channel request. +// If an error is returned, the channel is rejected. If a RawMessage is +// returned, it will be sent as the ChannelResult message. +// Remote -> [Open Authentication Channel] -> Local +func (ah *Server3DHAuthChannel) OpenInbound(channel *channels.Channel, oc *Protocol_Data_Control.OpenChannel) ([]byte, error) { + ah.channel = channel + clientPublicKey, _ := proto.GetExtension(oc, Protocol_Data_Auth_TripleEDH.E_ClientPublicKey) + clientEphmeralPublicKey, _ := proto.GetExtension(oc, Protocol_Data_Auth_TripleEDH.E_ClientEphmeralPublicKey) + clientPubKeyBytes := clientPublicKey.([]byte) + ah.clientPubKey = ed25519.PublicKey(clientPubKeyBytes[:]) + + clientEphmeralPublicKeyBytes := clientEphmeralPublicKey.([]byte) + ah.clientEphmeralPublicKey = ed25519.PublicKey(clientEphmeralPublicKeyBytes[:]) + + clientHostname := utils.GetTorV3Hostname(clientPubKeyBytes) + log.Printf("Received inbound auth 3DH request from %v", clientHostname) + + // Generate Ephemeral Keys + pubkey, privkey, _ := ed25519.GenerateKey(rand.Reader) + ah.serverEphemeralPublicKey = pubkey + ah.serverEphemeralPrivateKey = privkey + + var serverPubKeyBytes, serverEphemeralPubKeyBytes [32]byte + copy(serverPubKeyBytes[:], ah.ServerIdentity.PublicKeyBytes()[:]) + copy(serverEphemeralPubKeyBytes[:], ah.serverEphemeralPublicKey[:]) + + messageBuilder := new(utils.MessageBuilder) + channel.Pending = false + return messageBuilder.Confirm3EDHAuthChannel(ah.channel.ID, serverPubKeyBytes, serverEphemeralPubKeyBytes), nil +} + +// OpenOutbound is the first method called for an outbound channel request. +// If an error is returned, the channel is not opened. If a RawMessage is +// returned, it will be sent as the OpenChannel message. +// Local -> [Open Authentication Channel] -> Remote +func (ah *Server3DHAuthChannel) OpenOutbound(channel *channels.Channel) ([]byte, error) { + return nil, errors.New("server is not allowed to open 3dh channels") +} + +// OpenOutboundResult is called when a response is received for an +// outbound OpenChannel request. If `err` is non-nil, the channel was +// rejected and Closed will be called immediately afterwards. `raw` +// contains the raw protocol message including any extension data. +// Input: Remote -> [ChannelResult] -> {Client} +// Output: {Client} -> [Proof] -> Remote +func (ah *Server3DHAuthChannel) OpenOutboundResult(err error, crm *Protocol_Data_Control.ChannelResult) { + // +} + +// Packet is called for each raw packet received on this channel. +// Input: Client -> [Proof] -> Remote +// OR +// Input: Remote -> [Result] -> Client +func (ah *Server3DHAuthChannel) Packet(data []byte) { + res := new(Protocol_Data_Auth_TripleEDH.Packet) + err := proto.Unmarshal(data[:], res) + + if err != nil { + ah.channel.CloseChannel() + return + } + + if res.GetProof() != nil && ah.channel.Direction == channels.Inbound { + + // Server Identity <-> Client Ephemeral + secret1 := ah.ServerIdentity.EDH(ah.clientEphmeralPublicKey) + + // Server Ephemeral <-> Client Identity + secret2 := utils.EDH(ah.serverEphemeralPrivateKey, ah.clientPubKey) + + // Ephemeral <-> Ephemeral + secret3 := utils.EDH(ah.serverEphemeralPrivateKey, ah.clientEphmeralPublicKey) + + var secret [96]byte + copy(secret[0:32], secret1[:]) + copy(secret[32:64], secret2[:]) + copy(secret[64:96], secret3[:]) + + pkey := pbkdf2.Key(secret[:], secret[:], 4096, 32, sha3.New512) + var key [32]byte + copy(key[:], pkey[:]) + var decryptNonce [24]byte + ciphertext := res.GetProof().GetProof() + + if len(ciphertext) > 24 { + copy(decryptNonce[:], ciphertext[:24]) + decrypted, ok := secretbox.Open(nil, ciphertext[24:], &decryptNonce, &key) + + if ok && string(decrypted) == "Hello World" { + allowed, known := ah.ServerAuthValid(utils.GetTorV3Hostname(ah.clientPubKey), ah.clientPubKey) + ah.channel.DelegateAuthorization() + log.Printf("3DH Session Decrypted OK. Authenticating Connection!") + messageBuilder := new(utils.MessageBuilder) + result := messageBuilder.AuthResult3DH(allowed, known) + ah.channel.SendMessage(result) + ah.channel.CloseChannel() + return + } + } + + messageBuilder := new(utils.MessageBuilder) + result := messageBuilder.AuthResult3DH(false, false) + ah.channel.SendMessage(result) + } + + // Any other combination of packets is completely invalid + // Fail the Authorization right here. + ah.channel.CloseChannel() +} diff --git a/channels/v3/inbound/3dhauthchannel_test.go b/channels/v3/inbound/3dhauthchannel_test.go new file mode 100644 index 0000000..c44243e --- /dev/null +++ b/channels/v3/inbound/3dhauthchannel_test.go @@ -0,0 +1,110 @@ +package inbound + +import ( + "crypto/rand" + "git.openprivacy.ca/openprivacy/libricochet-go/channels" + "git.openprivacy.ca/openprivacy/libricochet-go/channels/v3/outbound" + "git.openprivacy.ca/openprivacy/libricochet-go/identity" + "git.openprivacy.ca/openprivacy/libricochet-go/wire/auth/3edh" + "git.openprivacy.ca/openprivacy/libricochet-go/wire/control" + "golang.org/x/crypto/ed25519" + + "github.com/golang/protobuf/proto" + "testing" +) + +func TestServer3DHAuthChannel(t *testing.T) { + + cc := new(channels.Channel) + cc.ID = 1 + cc.CloseChannel = func() {} + clientChannel := new(outbound.Client3DHAuthChannel) + pub, priv, _ := ed25519.GenerateKey(rand.Reader) + cid := identity.InitializeV3("", &priv, &pub) + clientChannel.ClientIdentity = cid + ocb, _ := clientChannel.OpenOutbound(cc) + + packet := new(Protocol_Data_Control.Packet) + proto.Unmarshal(ocb, packet) + + s3dhchannel := new(Server3DHAuthChannel) + pub, priv, _ = ed25519.GenerateKey(rand.Reader) + sid := identity.InitializeV3("", &priv, &pub) + s3dhchannel.ServerIdentity = sid + cr, _ := s3dhchannel.OpenInbound(cc, packet.GetOpenChannel()) + + proto.Unmarshal(cr, packet) + if packet.GetChannelResult() != nil { + + authPacket := new(Protocol_Data_Auth_TripleEDH.Packet) + var lastMessage []byte + cc.SendMessage = func(message []byte) { + proto.Unmarshal(message, authPacket) + lastMessage = message + } + clientChannel.OpenOutboundResult(nil, packet.GetChannelResult()) + if authPacket.Proof == nil { + t.Errorf("Was expected a Proof Packet, instead %v", authPacket) + } + + s3dhchannel.ServerAuthValid = func(hostname string, publicKey ed25519.PublicKey) (allowed, known bool) { + if hostname != clientChannel.ClientIdentity.Hostname() { + t.Errorf("Hostname and public key did not match %v %v", hostname, pub) + } + return true, true + } + cc.DelegateAuthorization = func() {} + s3dhchannel.Packet(lastMessage) + + } else { + t.Errorf("Should have received a Channel Response from OpenInbound: %v", packet) + } +} + +func TestServer3DHAuthChannelReject(t *testing.T) { + + cc := new(channels.Channel) + cc.ID = 1 + cc.CloseChannel = func() {} + clientChannel := new(outbound.Client3DHAuthChannel) + pub, priv, _ := ed25519.GenerateKey(rand.Reader) + cid := identity.InitializeV3("", &priv, &pub) + clientChannel.ClientIdentity = cid + ocb, _ := clientChannel.OpenOutbound(cc) + + packet := new(Protocol_Data_Control.Packet) + proto.Unmarshal(ocb, packet) + + s3dhchannel := new(Server3DHAuthChannel) + pub, priv, _ = ed25519.GenerateKey(rand.Reader) + sid := identity.InitializeV3("", &priv, &pub) + s3dhchannel.ServerIdentity = sid + cr, _ := s3dhchannel.OpenInbound(cc, packet.GetOpenChannel()) + + proto.Unmarshal(cr, packet) + if packet.GetChannelResult() != nil { + + authPacket := new(Protocol_Data_Auth_TripleEDH.Packet) + var lastMessage []byte + cc.SendMessage = func(message []byte) { + proto.Unmarshal(message, authPacket) + // Replace the Auth Proof Packet to cause this to fail. + if authPacket.GetProof() != nil { + authPacket.GetProof().Proof = []byte{} + lastMessage, _ = proto.Marshal(authPacket) + } + } + clientChannel.OpenOutboundResult(nil, packet.GetChannelResult()) + if authPacket.Proof == nil { + t.Errorf("Was expected a Proof Packet, instead %v", authPacket) + } + + s3dhchannel.ServerAuthInvalid = func(err error) { + } + cc.DelegateAuthorization = func() {} + s3dhchannel.Packet(lastMessage) + + } else { + t.Errorf("Should have received a Channel Response from OpenInbound: %v", packet) + } +} diff --git a/channels/v3/outbound/3dauthchannel.go b/channels/v3/outbound/3dauthchannel.go new file mode 100644 index 0000000..cb278dc --- /dev/null +++ b/channels/v3/outbound/3dauthchannel.go @@ -0,0 +1,173 @@ +package outbound + +import ( + "crypto/rand" + "errors" + "git.openprivacy.ca/openprivacy/libricochet-go/channels" + "git.openprivacy.ca/openprivacy/libricochet-go/identity" + "git.openprivacy.ca/openprivacy/libricochet-go/utils" + "git.openprivacy.ca/openprivacy/libricochet-go/wire/auth/3edh" + "git.openprivacy.ca/openprivacy/libricochet-go/wire/control" + "github.com/golang/protobuf/proto" + "golang.org/x/crypto/ed25519" + "golang.org/x/crypto/nacl/secretbox" + "golang.org/x/crypto/pbkdf2" + "golang.org/x/crypto/sha3" + "io" + "log" +) + +// Client3DHAuthChannel wraps implementation of im.ricochet.auth.hidden-service" +type Client3DHAuthChannel struct { + // PrivateKey must be set for client-side authentication channels + ClientIdentity identity.Identity + ServerHostname string + ClientAuthResult func(bool, bool) + // Internal state + serverPubKey, serverEphemeralPublicKey, clientEphemeralPublicKey ed25519.PublicKey + clientEphemeralPrivateKey ed25519.PrivateKey + channel *channels.Channel +} + +// Type returns the type string for this channel, e.g. "im.ricochet.chat". +func (ah *Client3DHAuthChannel) Type() string { + return "im.ricochet.auth.3dh" +} + +// Singleton Returns whether or not the given channel type is a singleton +func (ah *Client3DHAuthChannel) Singleton() bool { + return true +} + +// OnlyClientCanOpen ... +func (ah *Client3DHAuthChannel) OnlyClientCanOpen() bool { + return true +} + +// Bidirectional Returns whether or not the given channel allows anyone to send messages +func (ah *Client3DHAuthChannel) Bidirectional() bool { + return false +} + +// RequiresAuthentication Returns whether or not the given channel type requires authentication +func (ah *Client3DHAuthChannel) RequiresAuthentication() string { + return "none" +} + +// Closed is called when the channel is closed for any reason. +func (ah *Client3DHAuthChannel) Closed(err error) { + +} + +// OpenInbound is the first method called for an inbound channel request. +// If an error is returned, the channel is rejected. If a RawMessage is +// returned, it will be sent as the ChannelResult message. +// Remote -> [Open Authentication Channel] -> Local +func (ah *Client3DHAuthChannel) OpenInbound(channel *channels.Channel, oc *Protocol_Data_Control.OpenChannel) ([]byte, error) { + return nil, errors.New("server is not allowed to open inbound auth.3dh channels") +} + +// OpenOutbound is the first method called for an outbound channel request. +// If an error is returned, the channel is not opened. If a RawMessage is +// returned, it will be sent as the OpenChannel message. +// Local -> [Open Authentication Channel] -> Remote +func (ah *Client3DHAuthChannel) OpenOutbound(channel *channels.Channel) ([]byte, error) { + ah.channel = channel + + log.Printf("Opening an outbound connection to %v", ah.ServerHostname) + + // Generate Ephemeral Keys + pubkey, privkey, _ := ed25519.GenerateKey(rand.Reader) + ah.clientEphemeralPublicKey = pubkey + ah.clientEphemeralPrivateKey = privkey + + messageBuilder := new(utils.MessageBuilder) + channel.Pending = false + + var clientPubKeyBytes, clientEphemeralPubKeyBytes [32]byte + copy(clientPubKeyBytes[:], ah.ClientIdentity.PublicKeyBytes()[:]) + copy(clientEphemeralPubKeyBytes[:], ah.clientEphemeralPublicKey[:]) + + return messageBuilder.Open3EDHAuthenticationChannel(ah.channel.ID, clientPubKeyBytes, clientEphemeralPubKeyBytes), nil +} + +// OpenOutboundResult is called when a response is received for an +// outbound OpenChannel request. If `err` is non-nil, the channel was +// rejected and Closed will be called immediately afterwards. `raw` +// contains the raw protocol message including any extension data. +// Input: Remote -> [ChannelResult] -> {Client} +// Output: {Client} -> [Proof] -> Remote +func (ah *Client3DHAuthChannel) OpenOutboundResult(err error, crm *Protocol_Data_Control.ChannelResult) { + + serverPublicKey, _ := proto.GetExtension(crm, Protocol_Data_Auth_TripleEDH.E_ServerPublicKey) + serverEphemeralPublicKey, _ := proto.GetExtension(crm, Protocol_Data_Auth_TripleEDH.E_ServerEphmeralPublicKey) + + + + serverPubKeyBytes := serverPublicKey.([]byte) + ah.serverPubKey = ed25519.PublicKey(serverPubKeyBytes[:]) + + if utils.GetTorV3Hostname(ah.serverPubKey) != ah.ServerHostname { + ah.channel.CloseChannel() + return + } + + serverEphmeralPublicKeyBytes := serverEphemeralPublicKey.([]byte) + ah.serverEphemeralPublicKey = ed25519.PublicKey(serverEphmeralPublicKeyBytes[:]) + + log.Printf("Public Keys Exchanged. Deriving Encryption Keys and Sending Encrypted Test Message") + + // Server Ephemeral <-> Client Identity + secret1 := utils.EDH(ah.clientEphemeralPrivateKey, ah.serverPubKey) + + // Server Identity <-> Client Ephemeral + secret2 := ah.ClientIdentity.EDH(ah.serverEphemeralPublicKey) + + // Ephemeral <-> Ephemeral + secret3 := utils.EDH(ah.clientEphemeralPrivateKey, ah.serverEphemeralPublicKey) + + var secret [96]byte + copy(secret[0:32], secret1[:]) + copy(secret[32:64], secret2[:]) + copy(secret[64:96], secret3[:]) + + var nonce [24]byte + if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil { + panic(err) + } + + pkey := pbkdf2.Key(secret[:], secret[:], 4096, 32, sha3.New512) + var key [32]byte + copy(key[:], pkey[:]) + encrypted := secretbox.Seal(nonce[:], []byte("Hello World"), &nonce, &key) + messageBuilder := new(utils.MessageBuilder) + proof := messageBuilder.Proof3DH(encrypted) + ah.channel.SendMessage(proof) +} + +// Packet is called for each raw packet received on this channel. +// Input: Client -> [Proof] -> Remote +// OR +// Input: Remote -> [Result] -> Client +func (ah *Client3DHAuthChannel) Packet(data []byte) { + res := new(Protocol_Data_Auth_TripleEDH.Packet) + err := proto.Unmarshal(data[:], res) + + if err != nil { + ah.channel.CloseChannel() + return + } + + if res.GetResult() != nil { + ah.ClientAuthResult(res.GetResult().GetAccepted(), res.GetResult().GetIsKnownContact()) + if res.GetResult().GetAccepted() { + log.Printf("3DH Session Accepted OK. Authenticated! Connection!") + ah.channel.DelegateAuthorization() + } + return + } + + // Any other combination of packets is completely invalid + // Fail the Authorization right here. + ah.channel.CloseChannel() +} diff --git a/connection/autoconnectionhandler_test.go b/connection/autoconnectionhandler_test.go index dfff85f..b62325c 100644 --- a/connection/autoconnectionhandler_test.go +++ b/connection/autoconnectionhandler_test.go @@ -1,10 +1,10 @@ package connection import ( - "github.com/golang/protobuf/proto" "git.openprivacy.ca/openprivacy/libricochet-go/channels" "git.openprivacy.ca/openprivacy/libricochet-go/utils" "git.openprivacy.ca/openprivacy/libricochet-go/wire/control" + "github.com/golang/protobuf/proto" "testing" ) diff --git a/connection/channelmanager.go b/connection/channelmanager.go index 035eac9..4ef369e 100644 --- a/connection/channelmanager.go +++ b/connection/channelmanager.go @@ -75,7 +75,6 @@ func (cm *ChannelManager) OpenChannelRequestFromPeer(channelID int32, chandler c } cm.lock.Unlock() - // Some channels only allow us to open one of them per connection if chandler.Singleton() && cm.Channel(chandler.Type(), channels.Inbound) != nil { return nil, utils.AttemptToOpenMoreThanOneSingletonChannelError diff --git a/connection/connection.go b/connection/connection.go index 29b8f1e..96ada7b 100644 --- a/connection/connection.go +++ b/connection/connection.go @@ -3,10 +3,10 @@ package connection import ( "context" "fmt" - "github.com/golang/protobuf/proto" "git.openprivacy.ca/openprivacy/libricochet-go/channels" "git.openprivacy.ca/openprivacy/libricochet-go/utils" "git.openprivacy.ca/openprivacy/libricochet-go/wire/control" + "github.com/golang/protobuf/proto" "io" "log" "sync" diff --git a/connection/connection_test.go b/connection/connection_test.go index 02c1e06..e1b8190 100644 --- a/connection/connection_test.go +++ b/connection/connection_test.go @@ -1,9 +1,11 @@ package connection import ( + "crypto/rand" "crypto/rsa" "git.openprivacy.ca/openprivacy/libricochet-go/identity" "git.openprivacy.ca/openprivacy/libricochet-go/utils" + "golang.org/x/crypto/ed25519" "net" "testing" "time" @@ -14,6 +16,11 @@ func ServerAuthValid(hostname string, publicKey rsa.PublicKey) (allowed, known b return true, true } +// Server +func ServerAuthValid3DH(hostname string, publicKey ed25519.PublicKey) (allowed, known bool) { + return true, true +} + func TestProcessAuthAsServer(t *testing.T) { ln, _ := net.Listen("tcp", "127.0.0.1:0") @@ -45,6 +52,99 @@ func TestProcessAuthAsServer(t *testing.T) { } } +func TestProcessAuthAs3DHServer(t *testing.T) { + + ln, _ := net.Listen("tcp", "127.0.0.1:0") + + pub, priv, _ := ed25519.GenerateKey(rand.Reader) + + go func() { + cconn, _ := net.Dial("tcp", ln.Addr().String()) + + cpub, cpriv, _ := ed25519.GenerateKey(rand.Reader) + + hostname := utils.GetTorV3Hostname(pub) + orc := NewOutboundConnection(cconn, hostname) + orc.TraceLog(true) + + known, err := HandleOutboundConnection(orc).ProcessAuthAsV3Client(identity.InitializeV3("", &cpriv, &cpub)) + if err != nil { + t.Errorf("Error while testing ProcessAuthAsClient (in ProcessAuthAsServer) %v", err) + return + } else if !known { + t.Errorf("Client should have been known to the server, instead known was: %v", known) + return + } + }() + + conn, _ := ln.Accept() + + rc := NewInboundConnection(conn) + err := HandleInboundConnection(rc).ProcessAuthAsV3Server(identity.InitializeV3("", &priv, &pub), ServerAuthValid3DH) + if err != nil { + t.Errorf("Error while testing ProcessAuthAsServer: %v", err) + } +} + +func TestProcessAuthAsV3ServerFail(t *testing.T) { + + ln, _ := net.Listen("tcp", "127.0.0.1:0") + + pub, priv, _ := ed25519.GenerateKey(rand.Reader) + + go func() { + cconn, _ := net.Dial("tcp", ln.Addr().String()) + + cpub, cpriv, _ := ed25519.GenerateKey(rand.Reader) + + + // Setting the RemoteHostname to the client pub key approximates a server sending the wrong public key. + hostname := utils.GetTorV3Hostname(cpub) + orc := NewOutboundConnection(cconn, hostname) + orc.TraceLog(true) + + HandleOutboundConnection(orc).ProcessAuthAsV3Client(identity.InitializeV3("", &cpriv, &cpub)) + }() + + conn, _ := ln.Accept() + + rc := NewInboundConnection(conn) + err := HandleInboundConnection(rc).ProcessAuthAsV3Server(identity.InitializeV3("", &priv, &pub), ServerAuthValid3DH) + if err == nil { + t.Errorf("Error while testing ProcessAuthAsServer: %v", err) + } +} + + +func TestProcessAuthAsV3ClientFail(t *testing.T) { + + ln, _ := net.Listen("tcp", "127.0.0.1:0") + + pub, priv, _ := ed25519.GenerateKey(rand.Reader) + + go func() { + cconn, _ := net.Dial("tcp", ln.Addr().String()) + + // Giving the client inconsistent keypair to make EDH fail + cpub, _, _ := ed25519.GenerateKey(rand.Reader) + _,cpriv, _ := ed25519.GenerateKey(rand.Reader) + + hostname := utils.GetTorV3Hostname(pub) + orc := NewOutboundConnection(cconn, hostname) + orc.TraceLog(true) + + HandleOutboundConnection(orc).ProcessAuthAsV3Client(identity.InitializeV3("", &cpriv, &cpub)) + }() + + conn, _ := ln.Accept() + + rc := NewInboundConnection(conn) + err := HandleInboundConnection(rc).ProcessAuthAsV3Server(identity.InitializeV3("", &priv, &pub), ServerAuthValid3DH) + if err == nil { + t.Errorf("Error while testing ProcessAuthAsServer: %v", err) + } +} + func TestProcessServerAuthFail(t *testing.T) { ln, _ := net.Listen("tcp", "127.0.0.1:0") diff --git a/connection/control_channel_test.go b/connection/control_channel_test.go index 5b555b5..6e4787f 100644 --- a/connection/control_channel_test.go +++ b/connection/control_channel_test.go @@ -1,10 +1,10 @@ package connection import ( - "github.com/golang/protobuf/proto" "git.openprivacy.ca/openprivacy/libricochet-go/channels" "git.openprivacy.ca/openprivacy/libricochet-go/utils" "git.openprivacy.ca/openprivacy/libricochet-go/wire/control" + "github.com/golang/protobuf/proto" "testing" ) diff --git a/connection/inboundconnectionhandler.go b/connection/inboundconnectionhandler.go index f235dcc..c359793 100644 --- a/connection/inboundconnectionhandler.go +++ b/connection/inboundconnectionhandler.go @@ -3,9 +3,11 @@ package connection import ( "crypto/rsa" "git.openprivacy.ca/openprivacy/libricochet-go/channels" + "git.openprivacy.ca/openprivacy/libricochet-go/channels/v3/inbound" "git.openprivacy.ca/openprivacy/libricochet-go/identity" "git.openprivacy.ca/openprivacy/libricochet-go/policies" "git.openprivacy.ca/openprivacy/libricochet-go/utils" + "golang.org/x/crypto/ed25519" "sync" ) @@ -88,3 +90,66 @@ func (ich *InboundConnectionHandler) ProcessAuthAsServer(identity identity.Ident return err } + +// ProcessAuthAsServer blocks until authentication has succeeded, failed, or the +// connection is closed. A non-nil error is returned in all cases other than successful +// and accepted authentication. +// +// ProcessAuthAsServer cannot be called at the same time as any other call to a Process +// function. Another Process function must be called after this function successfully +// returns to continue handling connection events. +// +// The acceptCallback function is called after receiving a valid authentication proof +// with the client's authenticated hostname and public key. acceptCallback must return +// true to accept authentication and allow the connection to continue, and also returns a +// boolean indicating whether the contact is known and recognized. Unknown contacts will +// assume they are required to send a contact request before any other activity. +func (ich *InboundConnectionHandler) ProcessAuthAsV3Server(v3identity identity.Identity, sach func(hostname string, publicKey ed25519.PublicKey) (allowed, known bool)) error { + + var breakOnce sync.Once + + var authAllowed, authKnown bool + var authHostname string + + onAuthValid := func(hostname string, publicKey ed25519.PublicKey) (allowed, known bool) { + authAllowed, authKnown = sach(hostname, publicKey) + if authAllowed { + authHostname = hostname + } + breakOnce.Do(func() { go ich.connection.Break() }) + return authAllowed, authKnown + } + onAuthInvalid := func(err error) { + // err is ignored at the moment + breakOnce.Do(func() { go ich.connection.Break() }) + } + + ach := new(AutoConnectionHandler) + ach.Init() + ach.RegisterChannelHandler("im.ricochet.auth.3dh", + func() channels.Handler { + return &inbound.Server3DHAuthChannel{ + ServerIdentity: v3identity, + ServerAuthValid: onAuthValid, + ServerAuthInvalid: onAuthInvalid, + } + }) + + // Ensure that the call to Process() cannot outlive this function, + // particularly for the case where the policy timeout expires + defer breakOnce.Do(func() { ich.connection.Break() }) + policy := policies.UnknownPurposeTimeout + err := policy.ExecuteAction(func() error { + return ich.connection.Process(ach) + }) + + if err == nil { + if authAllowed == true { + ich.connection.RemoteHostname = authHostname + return nil + } + return utils.ClientFailedToAuthenticateError + } + + return err +} diff --git a/connection/outboundconnectionhandler.go b/connection/outboundconnectionhandler.go index d6b4876..90b7bd2 100644 --- a/connection/outboundconnectionhandler.go +++ b/connection/outboundconnectionhandler.go @@ -2,6 +2,7 @@ package connection import ( "git.openprivacy.ca/openprivacy/libricochet-go/channels" + "git.openprivacy.ca/openprivacy/libricochet-go/channels/v3/outbound" "git.openprivacy.ca/openprivacy/libricochet-go/identity" "git.openprivacy.ca/openprivacy/libricochet-go/policies" "git.openprivacy.ca/openprivacy/libricochet-go/utils" @@ -88,3 +89,67 @@ func (och *OutboundConnectionHandler) ProcessAuthAsClient(identity identity.Iden } return false, utils.ServerRejectedClientConnectionError } + +// ProcessAuthAs3DGClient blocks until authentication has succeeded or failed with the +// provided identity, or the connection is closed. A non-nil error is returned in all +// cases other than successful authentication. +// +// ProcessAuthAsClient cannot be called at the same time as any other call to a Porcess +// function. Another Process function must be called after this function successfully +// returns to continue handling connection events. +// +// For successful authentication, the `known` return value indicates whether the peer +// accepts us as a known contact. Unknown contacts will generally need to send a contact +// request before any other activity. +func (och *OutboundConnectionHandler) ProcessAuthAsV3Client(v3identity identity.Identity) (bool, error) { + + ach := new(AutoConnectionHandler) + ach.Init() + + // Make sure that calls to Break in this function cannot race + var breakOnce sync.Once + + var accepted, isKnownContact bool + authCallback := func(accept, known bool) { + accepted = accept + isKnownContact = known + // Cause the Process() call below to return. + // If Break() is called from here, it _must_ use go, because this will + // execute in the Process goroutine, and Break() will deadlock. + breakOnce.Do(func() { go och.connection.Break() }) + } + + processResult := make(chan error, 1) + go func() { + // Break Process() if timed out; no-op if Process returned a conn error + defer func() { breakOnce.Do(func() { och.connection.Break() }) }() + policy := policies.UnknownPurposeTimeout + err := policy.ExecuteAction(func() error { + return och.connection.Process(ach) + }) + processResult <- err + }() + + err := och.connection.Do(func() error { + _, err := och.connection.RequestOpenChannel("im.ricochet.auth.3dh", + &outbound.Client3DHAuthChannel{ + ClientIdentity: v3identity, + ServerHostname: och.connection.RemoteHostname, + ClientAuthResult: authCallback, + }) + return err + }) + if err != nil { + breakOnce.Do(func() { och.connection.Break() }) + return false, err + } + + if err = <-processResult; err != nil { + return false, err + } + + if accepted == true { + return isKnownContact, nil + } + return false, utils.ServerRejectedClientConnectionError +} diff --git a/identity/identity.go b/identity/identity.go index ff0ff67..5d2db3f 100644 --- a/identity/identity.go +++ b/identity/identity.go @@ -5,6 +5,7 @@ import ( "crypto/rsa" "encoding/asn1" "git.openprivacy.ca/openprivacy/libricochet-go/utils" + "golang.org/x/crypto/ed25519" ) // Identity is an encapsulation of Name, PrivateKey and other features @@ -12,8 +13,10 @@ import ( // 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 - pk *rsa.PrivateKey + Name string + pk *rsa.PrivateKey + edpk *ed25519.PrivateKey + edpubk *ed25519.PublicKey } // Init loads an identity from a file. Currently file should be a private_key @@ -21,28 +24,40 @@ type Identity struct { func Init(filename string) Identity { pk, err := utils.LoadPrivateKeyFromFile(filename) if err == nil { - return Identity{"", pk} + return Identity{"", pk, nil, nil} } return Identity{} } // Initialize is a courtesy function for initializing an Identity in-code. func Initialize(name string, pk *rsa.PrivateKey) Identity { - return Identity{name, pk} + return Identity{name, pk, nil, nil} +} + +// Initialize is a courtesy function for initializing an Identity in-code. +func InitializeV3(name string, pk *ed25519.PrivateKey, pubk *ed25519.PublicKey) Identity { + return Identity{name, nil, pk, pubk} } // Initialized ensures that an Identity has been assigned a private_key and // is ready to perform operations. func (i *Identity) Initialized() bool { if i.pk == nil { - return false + if i.edpk == nil { + return false + } } return true } // PublicKeyBytes returns the public key associated with this Identity in serializable-friendly -// format. //TODO Not sure I like this. +// format. func (i *Identity) PublicKeyBytes() []byte { + + if i.edpk != nil { + return *i.edpubk + } + publicKeyBytes, _ := asn1.Marshal(rsa.PublicKey{ N: i.pk.PublicKey.N, E: i.pk.PublicKey.E, @@ -51,9 +66,18 @@ func (i *Identity) PublicKeyBytes() []byte { return publicKeyBytes } +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.GetTorHostname(i.PublicKeyBytes()) + if i.edpk != nil { + return utils.GetTorV3Hostname(*i.edpubk) + } else { + return utils.GetTorHostname(i.PublicKeyBytes()) + } } // Sign produces a cryptographic signature using this Identities private key. diff --git a/testing/tests.sh b/testing/tests.sh index c161f73..461d159 100755 --- a/testing/tests.sh +++ b/testing/tests.sh @@ -2,12 +2,5 @@ set -e pwd -go test ${1} -coverprofile=main.cover.out -v . -go test ${1} -coverprofile=utils.cover.out -v ./utils -go test ${1} -coverprofile=channels.cover.out -v ./channels -go test ${1} -coverprofile=connection.cover.out -v ./connection -go test ${1} -coverprofile=policies.cover.out -v ./policies -go test ${1} -coverprofile=policies.cover.out -v ./identity -echo "mode: set" > coverage.out && cat *.cover.out | grep -v mode: | sort -r | \ -awk '{if($1 != last) {print $0;last=$1}}' >> coverage.out -rm -rf *.cover.out +go test ${1} -coverprofile=coverage.out -coverpkg="git.openprivacy.ca/openprivacy/libricochet-go/channels,git.openprivacy.ca/openprivacy/libricochet-go/channels/v3/inbound,git.openprivacy.ca/openprivacy/libricochet-go/channels/v3/outbound,git.openprivacy.ca/openprivacy/libricochet-go/identity,git.openprivacy.ca/openprivacy/libricochet-go/utils,git.openprivacy.ca/openprivacy/libricochet-go/policies,git.openprivacy.ca/openprivacy/libricochet-go/connection,git.openprivacy.ca/openprivacy/libricochet-go" -v . ./utils/ ./channels/ ./channels/v3/inbound ./connection ./policies ./identity ./utils + diff --git a/utils/crypto.go b/utils/crypto.go index 2970349..680d98c 100644 --- a/utils/crypto.go +++ b/utils/crypto.go @@ -6,7 +6,10 @@ import ( "crypto/x509" "encoding/pem" "errors" + "github.com/agl/ed25519/extra25519" "github.com/yawning/bulb/utils/pkcs1" + "golang.org/x/crypto/curve25519" + "golang.org/x/crypto/ed25519" "io/ioutil" "math" "math/big" @@ -30,6 +33,21 @@ func GetRandNumber() *big.Int { return num } +// EDH implements diffie hellman using curve25519 keys derived from ed25519 keys +// NOTE: This uses a 3rd party library extra25519 as the key conversion is not in the core golang lib +// as such this definitely needs further review. +func EDH(privateKey ed25519.PrivateKey, remotePublicKey ed25519.PublicKey) [32]byte { + var privKeyBytes [64]byte + var remotePubKeyBytes [32]byte + copy(privKeyBytes[:], privateKey[:]) + copy(remotePubKeyBytes[:], remotePublicKey[:]) + var secret, curve25519priv, curve25519pub [32]byte + extra25519.PrivateKeyToCurve25519(&curve25519priv, &privKeyBytes) + extra25519.PublicKeyToCurve25519(&curve25519pub, &remotePubKeyBytes) + curve25519.ScalarMult(&secret, &curve25519priv, &curve25519pub) + return secret +} + // GeneratePrivateKey generates a new private key for use func GeneratePrivateKey() (*rsa.PrivateKey, error) { privateKey, err := rsa.GenerateKey(rand.Reader, RicochetKeySize) diff --git a/utils/crypto_test.go b/utils/crypto_test.go index e827f5c..bf285ce 100644 --- a/utils/crypto_test.go +++ b/utils/crypto_test.go @@ -1,6 +1,8 @@ package utils import ( + "crypto/rand" + "golang.org/x/crypto/ed25519" "math" "testing" ) @@ -23,6 +25,16 @@ func TestLoadPrivateKey(t *testing.T) { } } +func TestEDH(t *testing.T) { + cpub, cpriv, _ := ed25519.GenerateKey(rand.Reader) + spub, spriv, _ := ed25519.GenerateKey(rand.Reader) + cedh := EDH(cpriv, spub) + sedh := EDH(spriv, cpub) + if string(cedh[:]) != string(sedh[:]) { + t.Errorf("Client and Server should see the same secret %v %v", cedh, sedh) + } +} + func TestGetRandNumber(t *testing.T) { num := GetRandNumber() if !num.IsUint64() || num.Uint64() > uint64(math.MaxUint32) { diff --git a/utils/messagebuilder.go b/utils/messagebuilder.go index ba35bc3..34527bf 100644 --- a/utils/messagebuilder.go +++ b/utils/messagebuilder.go @@ -1,11 +1,12 @@ package utils import ( - "github.com/golang/protobuf/proto" "git.openprivacy.ca/openprivacy/libricochet-go/wire/auth" + "git.openprivacy.ca/openprivacy/libricochet-go/wire/auth/3edh" "git.openprivacy.ca/openprivacy/libricochet-go/wire/chat" "git.openprivacy.ca/openprivacy/libricochet-go/wire/contact" "git.openprivacy.ca/openprivacy/libricochet-go/wire/control" + "github.com/golang/protobuf/proto" ) // MessageBuilder allows a client to construct specific data packets for the @@ -79,6 +80,63 @@ func (mb *MessageBuilder) ConfirmAuthChannel(channelID int32, serverCookie [16]b return ret } +// Open3EDHAuthenticationChannel constructs a message which will reuqest to open a channel for +// authentication on the given channelID, with the given cookie +func (mb *MessageBuilder) Open3EDHAuthenticationChannel(channelID int32, pubkey [32]byte, ephemeralKey [32]byte) []byte { + oc := &Protocol_Data_Control.OpenChannel{ + ChannelIdentifier: proto.Int32(channelID), + ChannelType: proto.String("im.ricochet.auth.3dh"), + } + err := proto.SetExtension(oc, Protocol_Data_Auth_TripleEDH.E_ClientPublicKey, pubkey[:]) + CheckError(err) + + err = proto.SetExtension(oc, Protocol_Data_Auth_TripleEDH.E_ClientEphmeralPublicKey, ephemeralKey[:]) + CheckError(err) + + pc := &Protocol_Data_Control.Packet{ + OpenChannel: oc, + } + ret, err := proto.Marshal(pc) + CheckError(err) + return ret +} + +// ConfirmAuthChannel constructs a message to acknowledge a previous open channel operation. +func (mb *MessageBuilder) Confirm3EDHAuthChannel(channelID int32, pubkey [32]byte, ephemeralKey [32]byte) []byte { + cr := &Protocol_Data_Control.ChannelResult{ + ChannelIdentifier: proto.Int32(channelID), + Opened: proto.Bool(true), + } + + err := proto.SetExtension(cr, Protocol_Data_Auth_TripleEDH.E_ServerPublicKey, pubkey[:]) + CheckError(err) + + err = proto.SetExtension(cr, Protocol_Data_Auth_TripleEDH.E_ServerEphmeralPublicKey, ephemeralKey[:]) + CheckError(err) + + pc := &Protocol_Data_Control.Packet{ + ChannelResult: cr, + } + ret, err := proto.Marshal(pc) + CheckError(err) + return ret +} + +// DHProof constructs a proof message with the given public key and signature. +func (mb *MessageBuilder) Proof3DH(proofBytes []byte) []byte { + proof := &Protocol_Data_Auth_TripleEDH.Proof{ + Proof: proofBytes, + } + + ahsPacket := &Protocol_Data_Auth_TripleEDH.Packet{ + Proof: proof, + } + + ret, err := proto.Marshal(ahsPacket) + CheckError(err) + return ret +} + // OpenContactRequestChannel contructs a message which will reuqest to open a channel for // a contact request on the given channelID, with the given nick and message. func (mb *MessageBuilder) OpenContactRequestChannel(channelID int32, nick string, message string) []byte { @@ -176,6 +234,24 @@ func (mb *MessageBuilder) Proof(publicKeyBytes []byte, signatureBytes []byte) [] return ret } +// AuthResult constructs a response to a Proof +func (mb *MessageBuilder) AuthResult3DH(accepted bool, isKnownContact bool) []byte { + // Construct a Result Message + result := &Protocol_Data_Auth_TripleEDH.Result{ + Accepted: proto.Bool(accepted), + IsKnownContact: proto.Bool(isKnownContact), + } + + ahsPacket := &Protocol_Data_Auth_TripleEDH.Packet{ + Proof: nil, + Result: result, + } + + ret, err := proto.Marshal(ahsPacket) + CheckError(err) + return ret +} + // AuthResult constructs a response to a Proof func (mb *MessageBuilder) AuthResult(accepted bool, isKnownContact bool) []byte { // Construct a Result Message diff --git a/utils/messagebuilder_test.go b/utils/messagebuilder_test.go index 0f930f9..858b348 100644 --- a/utils/messagebuilder_test.go +++ b/utils/messagebuilder_test.go @@ -1,8 +1,8 @@ package utils import ( - "github.com/golang/protobuf/proto" "git.openprivacy.ca/openprivacy/libricochet-go/wire/control" + "github.com/golang/protobuf/proto" "testing" ) diff --git a/utils/tor.go b/utils/tor.go index 28ab056..7174d1a 100644 --- a/utils/tor.go +++ b/utils/tor.go @@ -2,7 +2,11 @@ package utils import ( "crypto/sha1" + "crypto/sha512" "encoding/base32" + "encoding/base64" + "golang.org/x/crypto/ed25519" + "golang.org/x/crypto/sha3" "strings" ) @@ -17,3 +21,39 @@ func GetTorHostname(publicKeyBytes []byte) string { data := base32.StdEncoding.EncodeToString(sha1bytes) return strings.ToLower(data[0:16]) } + +// Expand ed25519.PrivateKey to (a || RH) form, return base64 +func expandKey(pri ed25519.PrivateKey) string { + h := sha512.Sum512(pri[:32]) + // Set bits so that h[:32] is private scalar "a" + h[0] &= 248 + h[31] &= 127 + h[31] |= 64 + // Since h[32:] is RH, h is now (a || RH) + return base64.StdEncoding.EncodeToString(h[:]) +} + +// Hidden service version +const version = byte(0x03) + +// Salt used to create checkdigits +const salt = ".onion checksum" + +func getCheckdigits(pub ed25519.PublicKey) []byte { + // Calculate checksum sha3(".onion checksum" || publicKey || version) + checkstr := []byte(salt) + checkstr = append(checkstr, pub...) + checkstr = append(checkstr, version) + checksum := sha3.Sum256(checkstr) + return checksum[:2] +} + +func GetTorV3Hostname(pub ed25519.PublicKey) string { + // Construct onion address base32(publicKey || checkdigits || version) + checkdigits := getCheckdigits(pub) + combined := pub[:] + combined = append(combined, checkdigits...) + combined = append(combined, version) + serviceID := base32.StdEncoding.EncodeToString(combined) + return strings.ToLower(serviceID) +} diff --git a/wire/auth/3edh/Auth3EDH.pb.go b/wire/auth/3edh/Auth3EDH.pb.go new file mode 100644 index 0000000..c9c9215 --- /dev/null +++ b/wire/auth/3edh/Auth3EDH.pb.go @@ -0,0 +1,135 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: Auth3EDH.proto + +/* +Package Protocol_Data_Auth_TripleEDH is a generated protocol buffer package. + +It is generated from these files: + Auth3EDH.proto + +It has these top-level messages: + Packet + Proof + Result +*/ +package Protocol_Data_Auth_TripleEDH + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" +import Protocol_Data_Control "git.openprivacy.ca/openprivacy/libricochet-go/wire/control" + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +type Packet struct { + Proof *Proof `protobuf:"bytes,1,opt,name=proof" json:"proof,omitempty"` + Result *Result `protobuf:"bytes,2,opt,name=result" json:"result,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *Packet) Reset() { *m = Packet{} } +func (m *Packet) String() string { return proto.CompactTextString(m) } +func (*Packet) ProtoMessage() {} + +func (m *Packet) GetProof() *Proof { + if m != nil { + return m.Proof + } + return nil +} + +func (m *Packet) GetResult() *Result { + if m != nil { + return m.Result + } + return nil +} + +type Proof struct { + Proof []byte `protobuf:"bytes,1,opt,name=proof" json:"proof,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *Proof) Reset() { *m = Proof{} } +func (m *Proof) String() string { return proto.CompactTextString(m) } +func (*Proof) ProtoMessage() {} + +func (m *Proof) GetProof() []byte { + if m != nil { + return m.Proof + } + return nil +} + +type Result struct { + Accepted *bool `protobuf:"varint,1,req,name=accepted" json:"accepted,omitempty"` + IsKnownContact *bool `protobuf:"varint,2,opt,name=is_known_contact,json=isKnownContact" json:"is_known_contact,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *Result) Reset() { *m = Result{} } +func (m *Result) String() string { return proto.CompactTextString(m) } +func (*Result) ProtoMessage() {} + +func (m *Result) GetAccepted() bool { + if m != nil && m.Accepted != nil { + return *m.Accepted + } + return false +} + +func (m *Result) GetIsKnownContact() bool { + if m != nil && m.IsKnownContact != nil { + return *m.IsKnownContact + } + return false +} + +var E_ClientPublicKey = &proto.ExtensionDesc{ + ExtendedType: (*Protocol_Data_Control.OpenChannel)(nil), + ExtensionType: ([]byte)(nil), + Field: 9200, + Name: "Protocol.Data.Auth.TripleEDH.client_public_key", + Tag: "bytes,9200,opt,name=client_public_key,json=clientPublicKey", + Filename: "Auth3EDH.proto", +} + +var E_ClientEphmeralPublicKey = &proto.ExtensionDesc{ + ExtendedType: (*Protocol_Data_Control.OpenChannel)(nil), + ExtensionType: ([]byte)(nil), + Field: 9300, + Name: "Protocol.Data.Auth.TripleEDH.client_ephmeral_public_key", + Tag: "bytes,9300,opt,name=client_ephmeral_public_key,json=clientEphmeralPublicKey", + Filename: "Auth3EDH.proto", +} + +var E_ServerPublicKey = &proto.ExtensionDesc{ + ExtendedType: (*Protocol_Data_Control.ChannelResult)(nil), + ExtensionType: ([]byte)(nil), + Field: 9200, + Name: "Protocol.Data.Auth.TripleEDH.server_public_key", + Tag: "bytes,9200,opt,name=server_public_key,json=serverPublicKey", + Filename: "Auth3EDH.proto", +} + +var E_ServerEphmeralPublicKey = &proto.ExtensionDesc{ + ExtendedType: (*Protocol_Data_Control.ChannelResult)(nil), + ExtensionType: ([]byte)(nil), + Field: 9300, + Name: "Protocol.Data.Auth.TripleEDH.server_ephmeral_public_key", + Tag: "bytes,9300,opt,name=server_ephmeral_public_key,json=serverEphmeralPublicKey", + Filename: "Auth3EDH.proto", +} + +func init() { + proto.RegisterType((*Packet)(nil), "Protocol.Data.Auth.TripleEDH.Packet") + proto.RegisterType((*Proof)(nil), "Protocol.Data.Auth.TripleEDH.Proof") + proto.RegisterType((*Result)(nil), "Protocol.Data.Auth.TripleEDH.Result") + proto.RegisterExtension(E_ClientPublicKey) + proto.RegisterExtension(E_ClientEphmeralPublicKey) + proto.RegisterExtension(E_ServerPublicKey) + proto.RegisterExtension(E_ServerEphmeralPublicKey) +} diff --git a/wire/auth/3edh/Auth3EDH.proto b/wire/auth/3edh/Auth3EDH.proto new file mode 100644 index 0000000..b65f23b --- /dev/null +++ b/wire/auth/3edh/Auth3EDH.proto @@ -0,0 +1,28 @@ +syntax = "proto2"; +package Protocol.Data.Auth.TripleEDH; +import "ControlChannel.proto"; + +extend Control.OpenChannel { + optional bytes client_public_key = 9200; + optional bytes client_ephmeral_public_key = 9300; +} + +extend Control.ChannelResult { + optional bytes server_public_key = 9200; + optional bytes server_ephmeral_public_key = 9300; +} + +message Packet { + optional Proof proof = 1; + optional Result result = 2; +} + +message Proof { + optional bytes proof = 1; // Encrypted Onion Address +} + +message Result { + required bool accepted = 1; + optional bool is_known_contact = 2; + +}