Browse Source

Moving towards a release candidate.

tags/v0.1.7
Sarah Jamie Lewis 4 months ago
parent
commit
63d6c1a1aa
13 changed files with 257 additions and 242 deletions
  1. 44
    0
      .drone.yml
  2. 1
    0
      .gitignore
  3. 1
    1
      application.go
  4. 16
    7
      applications/auth.go
  5. 83
    0
      applications/auth_test.go
  6. 2
    3
      cmd/main.go
  7. 9
    8
      networks/tor/BaseOnionService.go
  8. 0
    192
      notifications/main.go
  9. 2
    2
      primitives/bloom.go
  10. 1
    0
      primitives/time.go
  11. 66
    29
      service.go
  12. 24
    0
      testing/quality.sh
  13. 8
    0
      testing/tests.sh

+ 44
- 0
.drone.yml View File

@@ -0,0 +1,44 @@
workspace:
base: /go
path: src/cwtch.im/tapir

pipeline:
fetch:
image: golang
commands:
- wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/master/tor/tor
- wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/master/tor/torrc
- chmod a+x tor
- go list ./... | xargs go get
- go get -u golang.org/x/lint/golint
quality:
image: golang
commands:
- go list ./... | xargs go vet
- go list ./... | xargs golint -set_exit_status
units-tests:
image: golang
commands:
- export PATH=$PATH:/go/src/cwtch.im/tapir
- sh testing/tests.sh
integ-test:
image: golang
commands:
- ./tor -f ./torrc
- sleep 15
- go test -v cwtch.im/tapir/testing
notify-email:
image: drillster/drone-email
host: build.openprivacy.ca
port: 25
skip_verify: true
from: drone@openprivacy.ca
when:
status: [ failure ]
notify-gogs:
image: openpriv/drone-gogs
when:
event: pull_request
status: [ success, changed, failure ]
secrets: [gogs_account_token]
gogs_url: https://git.openprivacy.ca

+ 1
- 0
.gitignore View File

@@ -1,3 +1,4 @@
vendor/
.idea
/tor/
coverage.out

+ 1
- 1
application.go View File

@@ -3,5 +3,5 @@ package tapir
// Application defines the interface for all Tapir Applications
type Application interface {
NewInstance() Application
Init(connection *Connection)
Init(connection Connection)
}

+ 16
- 7
applications/auth.go View File

@@ -33,8 +33,8 @@ func (ea AuthApp) NewInstance() tapir.Application {

// Init runs the entire AuthApp protocol, at the end of the protocol either the connection is granted AUTH capability
// or the connection is closed.
func (ea AuthApp) Init(connection *tapir.Connection) {
longTermPubKey := ed25519.PublicKey(connection.ID.PublicKeyBytes())
func (ea AuthApp) Init(connection tapir.Connection) {
longTermPubKey := ed25519.PublicKey(connection.ID().PublicKeyBytes())
epk, esk, _ := ed25519.GenerateKey(rand.Reader)
ephemeralPublicKey := ed25519.PublicKey(epk)
ephemeralPrivateKey := ed25519.PrivateKey(esk)
@@ -52,13 +52,13 @@ func (ea AuthApp) Init(connection *tapir.Connection) {
}

// 3DH Handshake
l2e := connection.ID.EDH(remoteAuthMessage.EphemeralPublicKey)
l2e := connection.ID().EDH(remoteAuthMessage.EphemeralPublicKey)
e2l := ephemeralIdentity.EDH(remoteAuthMessage.LongTermPublicKey)
e2e := ephemeralIdentity.EDH(remoteAuthMessage.EphemeralPublicKey)

// We need to define an order for the result concatenation so that both sides derive the same key.
var result [96]byte
if connection.Outbound {
if connection.IsOutbound() {
copy(result[0:32], l2e)
copy(result[32:64], e2l)
copy(result[64:96], e2e)
@@ -69,15 +69,24 @@ func (ea AuthApp) Init(connection *tapir.Connection) {
}
connection.SetEncryptionKey(sha3.Sum256(result[:]))

// Wait to Sync
// Wait to Sync (we need to ensure that both the Local and Remote server have turned encryption on
// otherwise our next Send will fail.
time.Sleep(time.Second)

// TODO: Replace this with proper transcript
// TODO: Replace this with proper transcript primitive
challengeRemote, err := json.Marshal(remoteAuthMessage)
if err != nil {
connection.Close()
return
}
challengeLocal, err := json.Marshal(authMessage)
if err != nil {
connection.Close()
return
}
challenge := sha3.New512()

if connection.Outbound {
if connection.IsOutbound() {
challenge.Write(challengeLocal)
challenge.Write(challengeRemote)
} else {

+ 83
- 0
applications/auth_test.go View File

@@ -0,0 +1,83 @@
package applications

import (
"crypto/rand"
"encoding/json"
"git.openprivacy.ca/openprivacy/libricochet-go/identity"
"golang.org/x/crypto/ed25519"
"testing"
)

type MockConnection struct {
id identity.Identity
outbound bool
}

func (mc *MockConnection) Init(outbound bool) {
pubkey, privateKey, _ := ed25519.GenerateKey(rand.Reader)
sk := ed25519.PrivateKey(privateKey)
pk := ed25519.PublicKey(pubkey)
mc.id = identity.InitializeV3("", &sk, &pk)
mc.outbound = outbound
return
}

func (MockConnection) Hostname() string {
panic("implement me")
}

func (mc MockConnection) IsOutbound() bool {
return mc.outbound
}

func (mc MockConnection) ID() *identity.Identity {
return &mc.id
}

func (mc MockConnection) Expect() []byte {
longTermPubKey := ed25519.PublicKey(mc.id.PublicKeyBytes())
epk, _, _ := ed25519.GenerateKey(rand.Reader)
ephemeralPublicKey := ed25519.PublicKey(epk)
//ephemeralPrivateKey := ed25519.PrivateKey(esk)
//ephemeralIdentity := identity.InitializeV3("", &ephemeralPrivateKey, &ephemeralPublicKey)
authMessage := AuthMessage{LongTermPublicKey: longTermPubKey, EphemeralPublicKey: ephemeralPublicKey}
serialized, _ := json.Marshal(authMessage)
return serialized
}

func (MockConnection) SetHostname(hostname string) {
panic("implement me")
}

func (MockConnection) HasCapability(name string) bool {
panic("implement me")
}

func (MockConnection) SetCapability(name string) {
panic("implement me")
}

func (MockConnection) SetEncryptionKey(key [32]byte) {
// no op
}

func (MockConnection) Send(message []byte) {
// no op
}

func (MockConnection) Close() {
// no op
}

func (MockConnection) IsClosed() bool {
panic("implement me")
}

func TestAuthApp_Failed(t *testing.T) {
var authApp AuthApp
ai := authApp.NewInstance()

mc := new(MockConnection)
mc.Init(true)
ai.Init(mc)
}

+ 2
- 3
cmd/main.go View File

@@ -25,7 +25,7 @@ func (ea SimpleApp) NewInstance() tapir.Application {
}

// Init is run when the connection is first started.
func (ea SimpleApp) Init(connection *tapir.Connection) {
func (ea SimpleApp) Init(connection tapir.Connection) {
// First run the Authentication App
ea.AuthApp.Init(connection)

@@ -44,9 +44,8 @@ func CheckConnection(service tapir.Service, hostname string) {
if err == nil {
log.Infof("Authed!")
return
} else {
log.Errorf("Error %v", err)
}
log.Errorf("Error %v", err)
time.Sleep(time.Second)
}
}

+ 9
- 8
networks/tor/BaseOnionService.go View File

@@ -34,14 +34,14 @@ func (s *BaseOnionService) Init(acn connectivity.ACN, sk ed25519.PrivateKey, id

// WaitForCapabilityOrClose blocks until the connection has the given capability or the underlying connection is closed
// (through error or user action)
func (s *BaseOnionService) WaitForCapabilityOrClose(cid string, name string) (*tapir.Connection, error) {
func (s *BaseOnionService) WaitForCapabilityOrClose(cid string, name string) (tapir.Connection, error) {
conn, err := s.GetConnection(cid)
if err == nil {
for {
if conn.HasCapability(name) {
return conn, nil
}
if conn.Closed {
if conn.IsClosed() {
return nil, errors.New("connection is closed")
}
time.Sleep(time.Millisecond * 200)
@@ -51,12 +51,12 @@ func (s *BaseOnionService) WaitForCapabilityOrClose(cid string, name string) (*t
}

// GetConnection returns a connection for a given hostname.
func (s *BaseOnionService) GetConnection(hostname string) (*tapir.Connection, error) {
var conn *tapir.Connection
func (s *BaseOnionService) GetConnection(hostname string) (tapir.Connection, error) {
var conn tapir.Connection
s.connections.Range(func(key, value interface{}) bool {
connection := value.(*tapir.Connection)
if connection.Hostname == hostname {
if !connection.Closed {
connection := value.(tapir.Connection)
if connection.Hostname() == hostname {
if !connection.IsClosed() {
conn = connection
return false
}
@@ -136,10 +136,11 @@ func (s *BaseOnionService) Listen(app tapir.Application) error {
return err
}

// Shutdown closes the service and ensures that any connections are closed.
func (s *BaseOnionService) Shutdown() {
s.ls.Close()
s.connections.Range(func(key, value interface{}) bool {
connection := value.(*tapir.Connection)
connection := value.(tapir.Connection)
connection.Close()
return true
})

+ 0
- 192
notifications/main.go View File

@@ -1,192 +0,0 @@
package main

import (
"bytes"
"compress/gzip"
"crypto/rand"
"crypto/sha512"
"cwtch.im/tapir"
"cwtch.im/tapir/applications"
"cwtch.im/tapir/networks/tor"
"cwtch.im/tapir/primitives"
"encoding/hex"
"encoding/json"
"git.openprivacy.ca/openprivacy/libricochet-go/connectivity"
"git.openprivacy.ca/openprivacy/libricochet-go/identity"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
"golang.org/x/crypto/ed25519"
"io/ioutil"
"os"
"time"
)

// This example implements a basic notification application which allows peers to notify each other of new messages without downloading
// the entire contents of the server.
// NOTE: Very Incomplete Prototype.

// Notification contains a Topic string and a Message.
type Notification struct {
Topic string // A hex encoded string of the hash of the topic string
Message string
}

// NotificationClient allows publishing and reading from the notifications server
type NotificationClient struct {
applications.AuthApp
connection *tapir.Connection
}

// NewInstance should always return a new instantiation of the application.
func (nc NotificationClient) NewInstance() tapir.Application {
app := new(NotificationClient)
return app
}

// Init is run when the connection is first started.
func (nc *NotificationClient) Init(connection *tapir.Connection) {
// First run the Authentication App
nc.AuthApp.Init(connection)
if connection.HasCapability(applications.AuthCapability) {
nc.connection = connection
}
}

// Publish transforms the given topic string into a hashed ID, and sends the ID along with the message
// NOTE: Server learns the hash of the topic (and therefore can correlate repeated use of the same topic)
func (nc NotificationClient) Publish(topic string, message string) {
log.Debugf("Sending Publish Request")
hashedTopic := sha512.Sum512([]byte(topic))
data, _ := json.Marshal(notificationRequest{RequestType: "Publish", RequestData: map[string]string{"Topic": hex.EncodeToString(hashedTopic[:])}})
nc.connection.Send([]byte(data))
}

// Check returns true if the server might have notifications related to the topic.
// This check reveals nothing about the topic to the server.
func (nc NotificationClient) Check(topic string) bool {
log.Debugf("Sending Filter Request")
// Get an updated bloom filter
data, _ := json.Marshal(notificationRequest{RequestType: "BloomFilter", RequestData: map[string]string{}})
nc.connection.Send(data)
response := nc.connection.Expect()
var bf []primitives.BloomFilter
r, _ := gzip.NewReader(bytes.NewReader(response))
bfb, _ := ioutil.ReadAll(r)
json.Unmarshal(bfb, &bf)

// Check the topic handle in the bloom filter
hashedTopic := sha512.Sum512([]byte(topic))
return bf[time.Now().Hour()].Check(hashedTopic[:])
}

type notificationRequest struct {
RequestType string
RequestData map[string]string
}

// NotificationsServer implements the metadata resistant notifications server
type NotificationsServer struct {
applications.AuthApp
Filter []*primitives.BloomFilter
timeProvider primitives.TimeProvider
}

const DefaultNumberOfBuckets = 24 // 1 per hour of the day

// NewInstance should always return a new instantiation of the application.
func (ns NotificationsServer) NewInstance() tapir.Application {
app := new(NotificationsServer)

app.timeProvider = new(primitives.OSTimeProvider)
app.Filter = make([]*primitives.BloomFilter, DefaultNumberOfBuckets)
for i := range app.Filter {
app.Filter[i] = new(primitives.BloomFilter)
app.Filter[i].Init(1024)
}
return app
}

// Configure overrides the default parameters for the Notification Server
func (ns NotificationsServer) Configure(timeProvider primitives.TimeProvider) {
ns.timeProvider = timeProvider
}

// Init initializes the application.
func (ns NotificationsServer) Init(connection *tapir.Connection) {
// First run the Authentication App
ns.AuthApp.Init(connection)
if connection.HasCapability(applications.AuthCapability) {
for {
request := connection.Expect()
var nr notificationRequest
json.Unmarshal(request, &nr)
log.Debugf("Received Request %v", nr)
switch nr.RequestType {
case "Publish":
log.Debugf("Received Publish Request")
topic := nr.RequestData["Topic"]
// message := nr.RequestData["Message"]
topicID, err := hex.DecodeString(topic)
if err == nil {
currentBucket := ns.timeProvider.GetCurrentTime().Hour()
ns.Filter[currentBucket].Insert(topicID)
}
case "BloomFilter":
log.Debugf("Received Filter Request")
response, _ := json.Marshal(ns.Filter)
var b bytes.Buffer
w := gzip.NewWriter(&b)
w.Write(response)
w.Close()
connection.Send(b.Bytes())
}
}
}
}

func main() {

log.SetLevel(log.LevelDebug)

// Connect to Tor
var acn connectivity.ACN
acn, _ = connectivity.StartTor("./", "")
acn.WaitTillBootstrapped()

// Generate Server Keys
pubkey, privateKey, _ := ed25519.GenerateKey(rand.Reader)
sk := ed25519.PrivateKey(privateKey)
pk := ed25519.PublicKey(pubkey)
id := identity.InitializeV3("server", &sk, &pk)

// Init a Client to Connect to the Server
go client(acn, pubkey)

rm
}

// Client will Connect and launch it's own Echo App goroutine.
func client(acn connectivity.ACN, key ed25519.PublicKey) {
pubkey, privateKey, _ := ed25519.GenerateKey(rand.Reader)
sk := ed25519.PrivateKey(privateKey)
pk := ed25519.PublicKey(pubkey)
id := identity.InitializeV3("client", &sk, &pk)
var client tapir.Service
client = new(tor.BaseOnionService)
client.Init(acn, sk, id)

cid, _ := client.Connect(utils.GetTorV3Hostname(key), new(NotificationClient))

conn, err := client.WaitForCapabilityOrClose(cid, applications.AuthCapability)
if err == nil {
log.Debugf("Client has Auth: %v", conn.HasCapability(applications.AuthCapability))
nc := conn.App.(*NotificationClient)

// Basic Demonstration of Notification
log.Infof("Publishing to #astronomy: %v", nc.Check("#astronomy"))
nc.Publish("#astronomy", "New #Astronomy Post!")
log.Infof("Checking #astronomy: %v", nc.Check("#astronomy"))
}

os.Exit(0)
}

+ 2
- 2
primitives/bloom.go View File

@@ -18,7 +18,7 @@ func (bf *BloomFilter) Init(m int16) {

// Hash transforms a message to a set of bit flips
// Supports up to m == 65535
func (bf BloomFilter) Hash(msg []byte) []int {
func (bf *BloomFilter) Hash(msg []byte) []int {
hash := sha256.Sum256(msg)

pos1a := (int(hash[0]) + int(hash[1]) + int(hash[2]) + int(hash[3])) % 0xFF
@@ -53,7 +53,7 @@ func (bf *BloomFilter) Insert(msg []byte) {

// Check returns true if the messages might be in the BloomFilter
// (No false positives, possible false negatives due to the probabilistic nature of the filter)
func (bf BloomFilter) Check(msg []byte) bool {
func (bf *BloomFilter) Check(msg []byte) bool {
pos := bf.Hash(msg)
if bf.B[pos[0]] && bf.B[pos[1]] && bf.B[pos[2]] && bf.B[pos[3]] {
return true

+ 1
- 0
primitives/time.go View File

@@ -15,6 +15,7 @@ type TimeProvider interface {
type OSTimeProvider struct {
}

// GetCurrentTime returns the time provided by the OS
func (ostp OSTimeProvider) GetCurrentTime() time.Time {
return time.Now()
}

+ 66
- 29
service.go View File

@@ -18,70 +18,107 @@ type Service interface {
Init(acn connectivity.ACN, privateKey ed25519.PrivateKey, identity identity.Identity)
Connect(hostname string, application Application) (bool, error)
Listen(application Application) error
GetConnection(connectionID string) (*Connection, error)
WaitForCapabilityOrClose(connectionID string, capability string) (*Connection, error)
GetConnection(connectionID string) (Connection, error)
WaitForCapabilityOrClose(connectionID string, capability string) (Connection, error)
Shutdown()
}

// Connection Interface
type Connection interface {
Hostname() string
IsOutbound() bool
ID() *identity.Identity
Expect() []byte
SetHostname(hostname string)
HasCapability(name string) bool
SetCapability(name string)
SetEncryptionKey(key [32]byte)
Send(message []byte)
Close()
IsClosed() bool
}

// Connection defines a Tapir Connection
type Connection struct {
Hostname string
type connection struct {
hostname string
conn net.Conn
capabilities sync.Map
encrypted bool
key [32]byte
App Application
ID identity.Identity
Outbound bool
Closed bool
identity *identity.Identity
outbound bool
closed bool
MaxLength int
}

// NewConnection creates a new Connection
func NewConnection(id identity.Identity, hostname string, outbound bool, conn net.Conn, app Application) *Connection {
connection := new(Connection)
connection.Hostname = hostname
func NewConnection(id identity.Identity, hostname string, outbound bool, conn net.Conn, app Application) Connection {
connection := new(connection)
connection.hostname = hostname
connection.conn = conn
connection.App = app
connection.ID = id
connection.Outbound = outbound
connection.identity = &id
connection.outbound = outbound
connection.MaxLength = 1024
go connection.App.Init(connection)
return connection
}

// ID returns an identity.Identity encapsulation (for the purposes of cryptographic protocols)
func (c *connection) ID() *identity.Identity {
return c.identity
}

// Hostname returns the hostname of the connection (if the connection has not been authorized it will return the
// temporary hostname identifier)
func (c *connection) Hostname() string {
return c.hostname
}

// IsOutbound returns true if this caller was the originator of the connection (i.e. the connection was started
// by calling Connect() rather than Accept()
func (c *connection) IsOutbound() bool {
return c.outbound
}

// IsClosed returns true if the connection is closed (connections cannot be reopened)
func (c *connection) IsClosed() bool {
return c.closed
}

// SetHostname sets the hostname on the connection
func (c *Connection) SetHostname(hostname string) {
log.Debugf("[%v -- %v] Asserting Remote Hostname: %v", c.ID.Hostname(), c.Hostname, hostname)
c.Hostname = hostname
func (c *connection) SetHostname(hostname string) {
log.Debugf("[%v -- %v] Asserting Remote Hostname: %v", c.identity.Hostname(), c.hostname, hostname)
c.hostname = hostname
}

// SetCapability sets a capability on the connection
func (c *Connection) SetCapability(name string) {
log.Debugf("[%v -- %v] Setting Capability %v", c.ID.Hostname(), c.Hostname, name)
func (c *connection) SetCapability(name string) {
log.Debugf("[%v -- %v] Setting Capability %v", c.identity.Hostname(), c.hostname, name)
c.capabilities.Store(name, true)
}

// HasCapability checks if the connection has a given capability
func (c *Connection) HasCapability(name string) bool {
func (c *connection) HasCapability(name string) bool {
_, ok := c.capabilities.Load(name)
return ok
}

// Close forcibly closes the connection
func (c *Connection) Close() {
func (c *connection) Close() {
c.conn.Close()
}

// Expect blocks and reads a single Tapir packet , from the connection.
func (c *Connection) Expect() []byte {
func (c *connection) Expect() []byte {
buffer := make([]byte, c.MaxLength)
n, err := io.ReadFull(c.conn, buffer)

if n != c.MaxLength || err != nil {
log.Errorf("[%v -> %v] Wire Error Reading, Read %d bytes, Error: %v", c.Hostname, c.ID.Hostname(), n, err)
log.Errorf("[%v -> %v] Wire Error Reading, Read %d bytes, Error: %v", c.hostname, c.identity.Hostname(), n, err)
c.conn.Close()
c.Closed = true
c.closed = true
return []byte{}
}

@@ -92,9 +129,9 @@ func (c *Connection) Expect() []byte {
if ok {
copy(buffer, decrypted)
} else {
log.Errorf("[%v -> %v] Error Decrypting Message On Wire", c.Hostname, c.ID.Hostname())
log.Errorf("[%v -> %v] Error Decrypting Message On Wire", c.hostname, c.identity.Hostname())
c.conn.Close()
c.Closed = true
c.closed = true
return []byte{}
}
}
@@ -104,13 +141,13 @@ func (c *Connection) Expect() []byte {
}

// SetEncryptionKey turns on application-level encryption on the connection using the given key.
func (c *Connection) SetEncryptionKey(key [32]byte) {
func (c *connection) SetEncryptionKey(key [32]byte) {
c.key = key
c.encrypted = true
}

// Send writes a given message to a Tapir packet (of 1024 bytes in length).
func (c *Connection) Send(message []byte) {
func (c *connection) Send(message []byte) {

buffer := make([]byte, c.MaxLength)
binary.PutUvarint(buffer[0:2], uint64(len(message)))
@@ -121,16 +158,16 @@ func (c *Connection) Send(message []byte) {
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
// TODO: Surface is Error
c.conn.Close()
c.Closed = true
c.closed = true
}
// MaxLength - 40 = MaxLength - 24 nonce bytes and 16 auth tag.
encrypted := secretbox.Seal(nonce[:], buffer[0:c.MaxLength-40], &nonce, &c.key)
copy(buffer, encrypted[0:c.MaxLength])
}
log.Debugf("[%v -> %v] Wire Send %x", c.ID.Hostname(), c.Hostname, buffer)
log.Debugf("[%v -> %v] Wire Send %x", c.identity.Hostname(), c.hostname, buffer)
_, err := c.conn.Write(buffer)
if err != nil {
c.conn.Close()
c.Closed = true
c.closed = true
}
}

+ 24
- 0
testing/quality.sh View File

@@ -0,0 +1,24 @@
#!/bin/sh

echo "Checking code quality (you want to see no output here)"
echo ""

echo "Vetting:"
go list ./... | xargs go vet

echo ""
echo "Linting:"

go list ./... | xargs golint


echo "Time to format"
gofmt -l -s -w .

# ineffassign (https://github.com/gordonklaus/ineffassign)
echo "Checking for ineffectual assignment of errors (unchecked errors...)"
ineffassign .

# misspell (https://github.com/client9/misspell/cmd/misspell)
echo "Checking for misspelled words..."
misspell . | grep -v "vendor/" | grep -v "go.sum" | grep -v ".idea"

+ 8
- 0
testing/tests.sh View File

@@ -0,0 +1,8 @@
#!/bin/bash

set -e
pwd
go test ${1} -coverprofile=applications.cover.out -v ./applications
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

Loading…
Cancel
Save