v3onions
Sarah Jamie Lewis 4 years ago
parent 1667868d8c
commit 5066380655
  1. 6
      application/acceptallcontactmanager.go
  2. 35
      application/application.go
  3. 2
      application/contactmanagerinterface.go
  4. 22
      application/examples/v3/main.go
  5. 35
      application/ricochetonion.go
  6. 2
      channels/chatchannel.go
  7. 2
      channels/chatchannel_test.go
  8. 2
      channels/contactrequestchannel.go
  9. 2
      channels/contactrequestchannel_test.go
  10. 2
      channels/hiddenserviceauthchannel.go
  11. 2
      channels/hiddenserviceauthchannel_test.go
  12. 172
      channels/v3/inbound/3dhauthchannel.go
  13. 110
      channels/v3/inbound/3dhauthchannel_test.go
  14. 173
      channels/v3/outbound/3dauthchannel.go
  15. 2
      connection/autoconnectionhandler_test.go
  16. 1
      connection/channelmanager.go
  17. 2
      connection/connection.go
  18. 100
      connection/connection_test.go
  19. 2
      connection/control_channel_test.go
  20. 65
      connection/inboundconnectionhandler.go
  21. 65
      connection/outboundconnectionhandler.go
  22. 38
      identity/identity.go
  23. 11
      testing/tests.sh
  24. 18
      utils/crypto.go
  25. 12
      utils/crypto_test.go
  26. 78
      utils/messagebuilder.go
  27. 2
      utils/messagebuilder_test.go
  28. 40
      utils/tor.go
  29. 135
      wire/auth/3edh/Auth3EDH.pb.go
  30. 28
      wire/auth/3edh/Auth3EDH.proto

@ -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"
}

@ -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

@ -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)
}

@ -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)
}

@ -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)
}

@ -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"
)

@ -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"
)

@ -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

@ -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"
)

@ -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"
)

@ -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"
)

@ -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()
}

@ -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)
}
}

@ -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()
}

@ -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"
)

@ -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

@ -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"

@ -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")

@ -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"
)

@ -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
}

@ -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
}

@ -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.

@ -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

@ -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)

@ -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) {

@ -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 :=