Compare commits

..

No commits in common. "master" and "master" have entirely different histories.

38 changed files with 348 additions and 878 deletions

View File

@ -1,73 +1,66 @@
--- workspace:
kind: pipeline base: /go
type: docker path: src/cwtch.im/tapir
name: linux-test
steps: pipeline:
- name: fetch fetch:
image: golang:1.17.5 when:
volumes: repo: cwtch.im/tapir
- name: deps branch: master
path: /go event: [ push, pull_request ]
image: golang
commands: commands:
- wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/master/tor/tor - wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/master/tor/tor
- wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/master/tor/torrc - wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/master/tor/torrc
- chmod a+x tor - chmod a+x tor
- export GO111MODULE=on - export GO111MODULE=on
- go mod download - go mod vendor
- go install honnef.co/go/tools/cmd/staticcheck@latest - go get -u golang.org/x/lint/golint
- name: quality quality:
image: golang:1.17.5 when:
volumes: repo: cwtch.im/tapir
- name: deps branch: master
path: /go event: [ push, pull_request ]
image: golang
commands: commands:
- staticcheck ./... - go list ./... | xargs go vet
- name: units-tests - go list ./... | xargs golint -set_exit_status
image: golang:1.17.5 units-tests:
volumes: when:
- name: deps repo: cwtch.im/tapir
path: /go branch: master
event: [ push, pull_request ]
image: golang
commands: commands:
- export PATH=`pwd`:$PATH - export PATH=$PATH:/go/src/cwtch.im/tapir
- sh testing/tests.sh - sh testing/tests.sh
- name: integ-test integ-test:
image: golang:1.17.5 when:
volumes: repo: cwtch.im/tapir
- name: deps branch: master
path: /go event: [ push, pull_request ]
image: golang
commands: commands:
- export PATH=`pwd`:$PATH - ./tor -f ./torrc
- go test -race -v git.openprivacy.ca/cwtch.im/tapir/testing - sleep 15
- name: notify-email - go test -race -v cwtch.im/tapir/testing
notify-email:
image: drillster/drone-email image: drillster/drone-email
host: build.openprivacy.ca host: build.openprivacy.ca
port: 25 port: 25
skip_verify: true skip_verify: true
from: drone@openprivacy.ca from: drone@openprivacy.ca
when: when:
repo: cwtch.im/tapir
branch: master
event: [ push, pull_request ]
status: [ failure ] status: [ failure ]
- name: notify-gogs notify-gogs:
image: openpriv/drone-gogs image: openpriv/drone-gogs
pull: if-not-exists
when: when:
repo: cwtch.im/tapir
branch: master
event: pull_request event: pull_request
status: [ success, changed, failure ] status: [ success, changed, failure ]
environment: secrets: [gogs_account_token]
GOGS_ACCOUNT_TOKEN: gogs_url: https://git.openprivacy.ca
from_secret: gogs_account_token
settings:
gogs_url: https://git.openprivacy.ca
volumes:
# gopath where bin and pkg lives to persist across steps
- name: deps
temp: {}
trigger:
repo: cwtch.im/tapir
branch: master
event:
- push
- pull_request
- tag

1
.gitignore vendored
View File

@ -7,4 +7,3 @@ coverage.out
*.db *.db
/applications/tokenboard/tor/ /applications/tokenboard/tor/
fuzzing/ fuzzing/
*.cover.out

View File

@ -1,8 +0,0 @@
MIT License
Copyright (c) 2019 Open Privacy Research Society
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,7 +1,7 @@
package tapir package tapir
import ( import (
"git.openprivacy.ca/cwtch.im/tapir/primitives/core" "cwtch.im/tapir/primitives/core"
) )
// Capability defines a status granted to a connection, from an application. That allows the connection to access // Capability defines a status granted to a connection, from an application. That allows the connection to access

View File

@ -1,7 +1,7 @@
package applications package applications
import ( import (
"git.openprivacy.ca/cwtch.im/tapir" "cwtch.im/tapir"
) )
// ApplicationChain is a meta-app that can be used to build complex applications from other applications // ApplicationChain is a meta-app that can be used to build complex applications from other applications
@ -40,17 +40,13 @@ func (appchain *ApplicationChain) NewInstance() tapir.Application {
func (appchain *ApplicationChain) Init(connection tapir.Connection) { func (appchain *ApplicationChain) Init(connection tapir.Connection) {
appchain.TranscriptApp.Init(connection) appchain.TranscriptApp.Init(connection)
for i, app := range appchain.apps { for i, app := range appchain.apps {
// propagate the transcript to the app
app.PropagateTranscript(appchain.transcript) app.PropagateTranscript(appchain.transcript)
// apply the app to the connection
connection.SetApp(app)
// initialize the application given the connection
app.Init(connection) app.Init(connection)
// if we hit our guard then carry on, otherwise close... if connection.HasCapability(appchain.capabilities[i]) == false {
if !connection.HasCapability(appchain.capabilities[i]) {
connection.Close() connection.Close()
return return
} }
connection.SetApp(app)
} }
} }

View File

@ -2,11 +2,11 @@ package applications
import ( import (
"crypto/subtle" "crypto/subtle"
"cwtch.im/tapir"
"cwtch.im/tapir/primitives"
"encoding/json" "encoding/json"
"git.openprivacy.ca/cwtch.im/tapir" "git.openprivacy.ca/openprivacy/libricochet-go/log"
"git.openprivacy.ca/cwtch.im/tapir/primitives" "git.openprivacy.ca/openprivacy/libricochet-go/utils"
torProvider "git.openprivacy.ca/openprivacy/connectivity/tor"
"git.openprivacy.ca/openprivacy/log"
"golang.org/x/crypto/ed25519" "golang.org/x/crypto/ed25519"
) )
@ -49,19 +49,14 @@ func (ea *AuthApp) Init(connection tapir.Connection) {
// If we are an outbound connection we can perform an additional check to ensure that the server sent us back the correct long term // If we are an outbound connection we can perform an additional check to ensure that the server sent us back the correct long term
// public key // public key
if connection.IsOutbound() && torProvider.GetTorV3Hostname(remoteAuthMessage.LongTermPublicKey) != connection.Hostname() { if connection.IsOutbound() && utils.GetTorV3Hostname(remoteAuthMessage.LongTermPublicKey) != connection.Hostname() {
log.Errorf("The remote server (%v) has attempted to authenticate with a different public key %v", connection.Hostname(), torProvider.GetTorV3Hostname(remoteAuthMessage.LongTermPublicKey)) log.Errorf("The remote server (%v) has attempted to authenticate with a different public key %v", connection.Hostname(), utils.GetTorV3Hostname(remoteAuthMessage.LongTermPublicKey))
connection.Close() connection.Close()
return return
} }
// Perform the triple-diffie-hellman exchange. // Perform the triple-diffie-hellman exchange.
key, err := primitives.Perform3DH(connection.ID(), &ephemeralIdentity, remoteAuthMessage.LongTermPublicKey, remoteAuthMessage.EphemeralPublicKey, connection.IsOutbound()) key := primitives.Perform3DH(connection.ID(), &ephemeralIdentity, remoteAuthMessage.LongTermPublicKey, remoteAuthMessage.EphemeralPublicKey, connection.IsOutbound())
if err != nil {
log.Errorf("Failed Auth Challenge %v", err)
connection.Close()
return
}
connection.SetEncryptionKey(key) connection.SetEncryptionKey(key)
// We just successfully unmarshaled both of these, so we can safely ignore the err return from these functions. // We just successfully unmarshaled both of these, so we can safely ignore the err return from these functions.
@ -76,11 +71,11 @@ func (ea *AuthApp) Init(connection tapir.Connection) {
if connection.IsOutbound() { if connection.IsOutbound() {
outboundHostname = connection.ID().Hostname() outboundHostname = connection.ID().Hostname()
inboundHostname = torProvider.GetTorV3Hostname(remoteAuthMessage.LongTermPublicKey) inboundHostname = utils.GetTorV3Hostname(remoteAuthMessage.LongTermPublicKey)
outboundAuthMessage = challengeLocal outboundAuthMessage = challengeLocal
inboundAuthMessage = challengeRemote inboundAuthMessage = challengeRemote
} else { } else {
outboundHostname = torProvider.GetTorV3Hostname(remoteAuthMessage.LongTermPublicKey) outboundHostname = utils.GetTorV3Hostname(remoteAuthMessage.LongTermPublicKey)
inboundHostname = connection.ID().Hostname() inboundHostname = connection.ID().Hostname()
outboundAuthMessage = challengeRemote outboundAuthMessage = challengeRemote
inboundAuthMessage = challengeLocal inboundAuthMessage = challengeLocal
@ -108,12 +103,12 @@ func (ea *AuthApp) Init(connection tapir.Connection) {
// encryption key and the same transcript challenge. // encryption key and the same transcript challenge.
connection.Send(append(challengeBytes, []byte(connection.ID().Hostname())...)) connection.Send(append(challengeBytes, []byte(connection.ID().Hostname())...))
remoteChallenge := connection.Expect() remoteChallenge := connection.Expect()
assertedHostname := torProvider.GetTorV3Hostname(remoteAuthMessage.LongTermPublicKey) assertedHostname := utils.GetTorV3Hostname(remoteAuthMessage.LongTermPublicKey)
if subtle.ConstantTimeCompare(append(challengeBytes, []byte(assertedHostname)...), remoteChallenge) == 1 { if subtle.ConstantTimeCompare(append(challengeBytes, []byte(assertedHostname)...), remoteChallenge) == 1 {
connection.SetHostname(assertedHostname) connection.SetHostname(assertedHostname)
connection.SetCapability(AuthCapability) connection.SetCapability(AuthCapability)
} else { } else {
log.Debugf("Failed Decrypt Challenge: [%x] [%x]\n", remoteChallenge, challengeBytes) log.Errorf("Failed Decrypt Challenge: [%x] [%x]\n", remoteChallenge, challengeBytes)
connection.Close() connection.Close()
} }
} }

View File

@ -2,9 +2,9 @@ package applications
import ( import (
"crypto/rand" "crypto/rand"
"cwtch.im/tapir"
"cwtch.im/tapir/primitives"
"encoding/json" "encoding/json"
"git.openprivacy.ca/cwtch.im/tapir"
"git.openprivacy.ca/cwtch.im/tapir/primitives"
"golang.org/x/crypto/ed25519" "golang.org/x/crypto/ed25519"
"testing" "testing"
) )
@ -17,6 +17,7 @@ type MockConnection struct {
func (mc *MockConnection) Init(outbound bool) { func (mc *MockConnection) Init(outbound bool) {
mc.id, _ = primitives.InitializeEphemeralIdentity() mc.id, _ = primitives.InitializeEphemeralIdentity()
mc.outbound = outbound mc.outbound = outbound
return
} }
func (mc MockConnection) Hostname() string { func (mc MockConnection) Hostname() string {
@ -58,9 +59,8 @@ func (MockConnection) SetEncryptionKey(key [32]byte) {
// no op // no op
} }
func (MockConnection) Send(message []byte) error { func (MockConnection) Send(message []byte) {
// no op // no op
return nil
} }
func (MockConnection) Close() { func (MockConnection) Close() {
@ -80,10 +80,6 @@ func (MockConnection) IsClosed() bool {
panic("implement me") panic("implement me")
} }
func (MockConnection) Broadcast(message []byte, capability tapir.Capability) error {
panic("implement me")
}
func TestAuthApp_Failed(t *testing.T) { func TestAuthApp_Failed(t *testing.T) {
var authApp AuthApp var authApp AuthApp
ai := authApp.NewInstance() ai := authApp.NewInstance()

View File

@ -2,10 +2,9 @@ package applications
import ( import (
"crypto/sha256" "crypto/sha256"
"git.openprivacy.ca/cwtch.im/tapir" "cwtch.im/tapir"
"git.openprivacy.ca/cwtch.im/tapir/primitives/core" "cwtch.im/tapir/primitives/core"
"git.openprivacy.ca/openprivacy/log" "git.openprivacy.ca/openprivacy/libricochet-go/log"
ristretto "github.com/gtank/ristretto255"
) )
// ProofOfWorkApplication forces the incoming connection to do proof of work before granting a capability // ProofOfWorkApplication forces the incoming connection to do proof of work before granting a capability
@ -67,20 +66,9 @@ func (powapp *ProofOfWorkApplication) solveChallenge(challenge []byte, prng core
var sum [32]byte var sum [32]byte
solution := []byte{} solution := []byte{}
solve := make([]byte, len(challenge)+32) solve := make([]byte, len(challenge)+32)
// reuse our allocation
buf := make([]byte, 64)
next := new(ristretto.Scalar)
encodedSolution := make([]byte, 0, 32)
for !solved { for !solved {
err := prng.Next(buf, next)
if err != nil {
// this will cause the challenge to fail...
log.Errorf("error completing challenge: %v", err)
return nil
}
//lint:ignore SA1019 API this is "deprecated", but without it it will cause an allocation on every single check solution = prng.Next().Encode(nil)
solution = next.Encode(encodedSolution)
copy(solve[0:], solution[:]) copy(solve[0:], solution[:])
copy(solve[len(solution):], challenge[:]) copy(solve[len(solution):], challenge[:])
@ -92,8 +80,6 @@ func (powapp *ProofOfWorkApplication) solveChallenge(challenge []byte, prng core
solved = false solved = false
} }
} }
// reuse this allocated memory next time...
encodedSolution = encodedSolution[:0]
} }
log.Debugf("Validated Challenge %v: %v %v\n", challenge, solution, sum) log.Debugf("Validated Challenge %v: %v %v\n", challenge, solution, sum)
return solution[:] return solution[:]
@ -101,9 +87,6 @@ func (powapp *ProofOfWorkApplication) solveChallenge(challenge []byte, prng core
// ValidateChallenge returns true if the message and spamguard pass the challenge // ValidateChallenge returns true if the message and spamguard pass the challenge
func (powapp *ProofOfWorkApplication) validateChallenge(challenge []byte, solution []byte) bool { func (powapp *ProofOfWorkApplication) validateChallenge(challenge []byte, solution []byte) bool {
if len(solution) != 32 {
return false
}
solve := make([]byte, len(challenge)+32) solve := make([]byte, len(challenge)+32)
copy(solve[0:], solution[0:32]) copy(solve[0:], solution[0:32])
copy(solve[32:], challenge[:]) copy(solve[32:], challenge[:])

View File

@ -1,10 +1,10 @@
package applications package applications
import ( import (
"cwtch.im/tapir"
"cwtch.im/tapir/primitives/privacypass"
"encoding/json" "encoding/json"
"git.openprivacy.ca/cwtch.im/tapir" "git.openprivacy.ca/openprivacy/libricochet-go/log"
"git.openprivacy.ca/cwtch.im/tapir/primitives/privacypass"
"git.openprivacy.ca/openprivacy/log"
) )
// TokenApplication provides Tokens for PoW // TokenApplication provides Tokens for PoW
@ -55,10 +55,7 @@ func (tokenapp *TokenApplication) Init(connection tapir.Connection) {
var blinded []privacypass.BlindedToken var blinded []privacypass.BlindedToken
err := json.Unmarshal(connection.Expect(), &blinded) err := json.Unmarshal(connection.Expect(), &blinded)
if err == nil { if err == nil {
batchProof, err := tokenapp.TokenService.SignBlindedTokenBatch(blinded, tokenapp.Transcript()) batchProof := tokenapp.TokenService.SignBlindedTokenBatch(blinded, tokenapp.Transcript())
if err != nil {
return
}
log.Debugf(tokenapp.Transcript().OutputTranscriptToAudit()) log.Debugf(tokenapp.Transcript().OutputTranscriptToAudit())
data, _ := json.Marshal(batchProof) data, _ := json.Marshal(batchProof)
connection.Send(data) connection.Send(data)

View File

@ -1,12 +1,12 @@
package tokenboard package tokenboard
import ( import (
"cwtch.im/tapir"
"cwtch.im/tapir/applications"
"cwtch.im/tapir/primitives/auditable"
"cwtch.im/tapir/primitives/privacypass"
"encoding/json" "encoding/json"
"git.openprivacy.ca/cwtch.im/tapir" "git.openprivacy.ca/openprivacy/libricochet-go/log"
"git.openprivacy.ca/cwtch.im/tapir/applications"
"git.openprivacy.ca/cwtch.im/tapir/primitives/auditable"
"git.openprivacy.ca/cwtch.im/tapir/primitives/privacypass"
"git.openprivacy.ca/openprivacy/log"
) )
// NewTokenBoardClient generates a new Client for Token Board // NewTokenBoardClient generates a new Client for Token Board

View File

@ -1,8 +1,8 @@
package tokenboard package tokenboard
import ( import (
"git.openprivacy.ca/cwtch.im/tapir/primitives/auditable" "cwtch.im/tapir/primitives/auditable"
"git.openprivacy.ca/cwtch.im/tapir/primitives/privacypass" "cwtch.im/tapir/primitives/privacypass"
) )
// AppHandler allows clients to react to specific events. // AppHandler allows clients to react to specific events.

View File

@ -3,12 +3,12 @@ package tokenboard
// NOTE: This is a sketch implementation, Not suitable for production use. The real auditable store is still being designed. // NOTE: This is a sketch implementation, Not suitable for production use. The real auditable store is still being designed.
import ( import (
"cwtch.im/tapir"
"cwtch.im/tapir/applications"
"cwtch.im/tapir/primitives/auditable"
"cwtch.im/tapir/primitives/privacypass"
"encoding/json" "encoding/json"
"git.openprivacy.ca/cwtch.im/tapir" "git.openprivacy.ca/openprivacy/libricochet-go/log"
"git.openprivacy.ca/cwtch.im/tapir/applications"
"git.openprivacy.ca/cwtch.im/tapir/primitives/auditable"
"git.openprivacy.ca/cwtch.im/tapir/primitives/privacypass"
"git.openprivacy.ca/openprivacy/log"
) )
// NewTokenBoardServer generates new Server for Token Board // NewTokenBoardServer generates new Server for Token Board

View File

@ -1,16 +1,15 @@
package tokenboard package tokenboard
import ( import (
"cwtch.im/tapir"
"cwtch.im/tapir/applications"
"cwtch.im/tapir/networks/tor"
"cwtch.im/tapir/primitives"
"cwtch.im/tapir/primitives/auditable"
"cwtch.im/tapir/primitives/privacypass"
"errors" "errors"
"git.openprivacy.ca/cwtch.im/tapir/applications" "git.openprivacy.ca/openprivacy/libricochet-go/connectivity"
"git.openprivacy.ca/cwtch.im/tapir/networks/tor" "git.openprivacy.ca/openprivacy/libricochet-go/log"
"git.openprivacy.ca/cwtch.im/tapir/primitives"
"git.openprivacy.ca/cwtch.im/tapir/primitives/auditable"
"git.openprivacy.ca/cwtch.im/tapir/primitives/privacypass"
"git.openprivacy.ca/openprivacy/connectivity"
torProvider "git.openprivacy.ca/openprivacy/connectivity/tor"
"git.openprivacy.ca/openprivacy/log"
"os"
"runtime" "runtime"
"sync" "sync"
"testing" "testing"
@ -38,7 +37,8 @@ type FreePaymentHandler struct {
func (fph *FreePaymentHandler) MakePayment() { func (fph *FreePaymentHandler) MakePayment() {
id, sk := primitives.InitializeEphemeralIdentity() id, sk := primitives.InitializeEphemeralIdentity()
client := new(tor.BaseOnionService) var client tapir.Service
client = new(tor.BaseOnionService)
client.Init(fph.ACN, sk, &id) client.Init(fph.ACN, sk, &id)
tokenApplication := new(applications.TokenApplication) tokenApplication := new(applications.TokenApplication)
@ -71,22 +71,9 @@ func TestTokenBoardApp(t *testing.T) {
// numRoutinesStart := runtime.NumGoroutine() // numRoutinesStart := runtime.NumGoroutine()
log.SetLevel(log.LevelDebug) log.SetLevel(log.LevelDebug)
log.Infof("Number of goroutines open at start: %d", runtime.NumGoroutine()) log.Infof("Number of goroutines open at start: %d", runtime.NumGoroutine())
os.MkdirAll("./tor/", 0700)
builder := new(torProvider.TorrcBuilder)
builder.WithSocksPort(9059).WithControlPort(9060).WithHashedPassword("tapir-integration-test").Build("./tor/torrc")
torDataDir := ""
var err error
if torDataDir, err = os.MkdirTemp("./tor/", "data-dir-"); err != nil {
t.Fatalf("could not create data dir")
}
// Connect to Tor // Connect to Tor
acn, err := torProvider.NewTorACNWithAuth("./", "", torDataDir, 9060, torProvider.HashedPasswordAuthenticator{Password: "tapir-integration-test"}) var acn connectivity.ACN
acn, _ = connectivity.StartTor("./", "")
if err != nil {
t.Fatalf("could not launch ACN %v", err)
}
acn.WaitTillBootstrapped() acn.WaitTillBootstrapped()
// Generate Server Key // Generate Server Key
@ -102,7 +89,8 @@ func TestTokenBoardApp(t *testing.T) {
clientAuditableStore.Init(publicsid) clientAuditableStore.Init(publicsid)
// Init the Server running the Simple App. // Init the Server running the Simple App.
service := new(tor.BaseOnionService) var service tapir.Service
service = new(tor.BaseOnionService)
service.Init(acn, sk, &sid) service.Init(acn, sk, &sid)
// Goroutine Management // Goroutine Management
@ -114,7 +102,8 @@ func TestTokenBoardApp(t *testing.T) {
}() }()
// Init the Server running the PoW Token App. // Init the Server running the PoW Token App.
powTokenService := new(tor.BaseOnionService) var powTokenService tapir.Service
powTokenService = new(tor.BaseOnionService)
spowid, spowk := primitives.InitializeEphemeralIdentity() spowid, spowk := primitives.InitializeEphemeralIdentity()
powTokenService.Init(acn, spowk, &spowid) powTokenService.Init(acn, spowk, &spowid)
sg.Add(1) sg.Add(1)
@ -130,7 +119,8 @@ func TestTokenBoardApp(t *testing.T) {
time.Sleep(time.Second * 60) // wait for server to initialize time.Sleep(time.Second * 60) // wait for server to initialize
id, sk := primitives.InitializeEphemeralIdentity() id, sk := primitives.InitializeEphemeralIdentity()
client := new(tor.BaseOnionService) var client tapir.Service
client = new(tor.BaseOnionService)
client.Init(acn, sk, &id) client.Init(acn, sk, &id)
client.Connect(sid.Hostname(), NewTokenBoardClient(clientAuditableStore, Handler{Store: clientAuditableStore}, &FreePaymentHandler{ACN: acn, TokenService: tokenService, ServerHostname: spowid.Hostname()})) client.Connect(sid.Hostname(), NewTokenBoardClient(clientAuditableStore, Handler{Store: clientAuditableStore}, &FreePaymentHandler{ACN: acn, TokenService: tokenService, ServerHostname: spowid.Hostname()}))
client.WaitForCapabilityOrClose(sid.Hostname(), applications.AuthCapability) client.WaitForCapabilityOrClose(sid.Hostname(), applications.AuthCapability)

View File

@ -1,9 +1,8 @@
package applications package applications
import ( import (
"git.openprivacy.ca/cwtch.im/tapir" "cwtch.im/tapir"
"git.openprivacy.ca/cwtch.im/tapir/primitives/core" "cwtch.im/tapir/primitives/core"
"git.openprivacy.ca/openprivacy/log"
) )
// TranscriptApp defines a Tapir Meta-App which provides a global cryptographic transcript // TranscriptApp defines a Tapir Meta-App which provides a global cryptographic transcript
@ -19,9 +18,6 @@ func (TranscriptApp) NewInstance() tapir.Application {
// Init initializes the cryptographic transcript // Init initializes the cryptographic transcript
func (ta *TranscriptApp) Init(connection tapir.Connection) { func (ta *TranscriptApp) Init(connection tapir.Connection) {
if ta.transcript != nil {
ta.panic()
}
ta.transcript = core.NewTranscript("tapir-transcript") ta.transcript = core.NewTranscript("tapir-transcript")
} }
@ -32,19 +28,5 @@ func (ta *TranscriptApp) Transcript() *core.Transcript {
// PropagateTranscript overrides the default transcript and propagates a transcript from a previous session // PropagateTranscript overrides the default transcript and propagates a transcript from a previous session
func (ta *TranscriptApp) PropagateTranscript(transcript *core.Transcript) { func (ta *TranscriptApp) PropagateTranscript(transcript *core.Transcript) {
if ta.transcript != nil {
ta.panic()
}
ta.transcript = transcript ta.transcript = transcript
} }
func (ta *TranscriptApp) panic() {
// Note: if this is ever happens it is a critical application bug
// This will prevent a misuse of application chains that cause an earlier
// transcript to be overwritten. Since we expect the security of many higher level applications
// to be reliant on the randomness provided by the transcript we want to be actively hostile to any potential
// misuse.
log.Errorf("apps should not attempt to intitalize or overwrite a transcript once one has been initialized - this is a CRITICAL bug and so we have safely crashed")
// We could silently fail to do anything here, but that is likely more dangerous in the long run...
panic("apps should not attempt to intitalize or overwrite a transcript a transcript once one has been initialized - this is a CRITICAL bug and so we have safely crashed")
}

View File

@ -1,22 +0,0 @@
package applications
import "testing"
func TestTranscriptApp(t *testing.T) {
ta := new(TranscriptApp)
ta.Init(MockConnection{})
ta.Transcript().NewProtocol("test")
ta.transcript.CommitToTranscript("test-commit")
t.Logf(ta.Transcript().OutputTranscriptToAudit())
// Now we test panic'ing....
defer func() {
if r := recover(); r == nil {
t.Errorf("The code did not panic - it definitely should have")
}
}()
// Attempt to reinitialized the transcript, apps should *never* do this and we want to be hostile to that
// behaviour
ta.Init(MockConnection{})
}

23
go.mod
View File

@ -1,21 +1,12 @@
module git.openprivacy.ca/cwtch.im/tapir module cwtch.im/tapir
go 1.17
require ( require (
filippo.io/edwards25519 v1.0.0 git.openprivacy.ca/openprivacy/libricochet-go v1.0.4
git.openprivacy.ca/openprivacy/connectivity v1.8.6
git.openprivacy.ca/openprivacy/log v1.0.3
github.com/gtank/merlin v0.1.1 github.com/gtank/merlin v0.1.1
github.com/gtank/ristretto255 v0.1.3-0.20210930101514-6bb39798585c github.com/gtank/ristretto255 v0.1.2
go.etcd.io/bbolt v1.3.6 go.etcd.io/bbolt v1.3.3
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f
golang.org/x/sync v0.0.0-20190423024810-112230192c58 // indirect
) )
require ( go 1.13
git.openprivacy.ca/openprivacy/bine v0.0.4 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b // indirect
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b // indirect
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 // indirect
)

69
go.sum
View File

@ -1,56 +1,37 @@
filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= git.openprivacy.ca/openprivacy/libricochet-go v1.0.4 h1:GWLMJ5jBSIC/gFXzdbbeVz7fIAn2FTgW8+wBci6/3Ek=
filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek= git.openprivacy.ca/openprivacy/libricochet-go v1.0.4/go.mod h1:yMSG1gBaP4f1U+RMZXN85d29D39OK5s8aTpyVRoH5FY=
filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 h1:w1UutsfOrms1J05zt7ISrnJIXKzwaspym5BTKGx93EI=
git.openprivacy.ca/openprivacy/bine v0.0.4 h1:CO7EkGyz+jegZ4ap8g5NWRuDHA/56KKvGySR6OBPW+c= github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0=
git.openprivacy.ca/openprivacy/bine v0.0.4/go.mod h1:13ZqhKyqakDsN/ZkQkIGNULsmLyqtXc46XBcnuXm/mU= github.com/cretz/bine v0.1.0 h1:1/fvhLE+fk0bPzjdO5Ci+0ComYxEMuB1JhM4X5skT3g=
git.openprivacy.ca/openprivacy/connectivity v1.8.6 h1:g74PyDGvpMZ3+K0dXy3mlTJh+e0rcwNk0XF8owzkmOA= github.com/cretz/bine v0.1.0/go.mod h1:6PF6fWAvYtwjRGkAuDEJeWNOv3a2hUouSP/yRYXmvHw=
git.openprivacy.ca/openprivacy/connectivity v1.8.6/go.mod h1:Hn1gpOx/bRZp5wvCtPQVJPXrfeUH0EGiG/Aoa0vjGLg= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
git.openprivacy.ca/openprivacy/log v1.0.3 h1:E/PMm4LY+Q9s3aDpfySfEDq/vYQontlvNj/scrPaga0=
git.openprivacy.ca/openprivacy/log v1.0.3/go.mod h1:gGYK8xHtndRLDymFtmjkG26GaMQNgyhioNS82m812Iw=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/gtank/merlin v0.1.1 h1:eQ90iG7K9pOhtereWsmyRJ6RAwcP4tHTDBHXNg+u5is= github.com/gtank/merlin v0.1.1 h1:eQ90iG7K9pOhtereWsmyRJ6RAwcP4tHTDBHXNg+u5is=
github.com/gtank/merlin v0.1.1/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s= github.com/gtank/merlin v0.1.1/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s=
github.com/gtank/ristretto255 v0.1.3-0.20210930101514-6bb39798585c h1:gkfmnY4Rlt3VINCo4uKdpvngiibQyoENVj5Q88sxXhE= github.com/gtank/ristretto255 v0.1.2 h1:JEqUCPA1NvLq5DwYtuzigd7ss8fwbYay9fi4/5uMzcc=
github.com/gtank/ristretto255 v0.1.3-0.20210930101514-6bb39798585c/go.mod h1:tDPFhGdt3hJWqtKwx57i9baiB1Cj0yAg22VOPUqm5vY= github.com/gtank/ristretto255 v0.1.2/go.mod h1:Ph5OpO6c7xKUGROZfWVLiJf9icMDwUeIvY4OmlYW69o=
github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643 h1:hLDRPB66XQT/8+wG9WsDpiCvZf1yKO7sz7scAjSlBa0=
github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643/go.mod h1:43+3pMjjKimDBf5Kr4ZFNGbLql1zKkbImw+fZbw3geM= github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643/go.mod h1:43+3pMjjKimDBf5Kr4ZFNGbLql1zKkbImw+fZbw3geM=
github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b h1:QrHweqAtyJ9EwCaGHBu1fghwxIPiopAHV06JlXrMHjk=
github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b/go.mod h1:xxLb2ip6sSUts3g1irPVHyk/DGslwQsNOo9I7smJfNU=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk=
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
golang.org/x/crypto v0.0.0-20190128193316-c7b33c32a30b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f h1:R423Cnkcp5JABoeemiGEPlt9tHXFfw5kvc0yqlxRPWo=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d h1:3qF+Z8Hkrw9sOhrFHti9TlB1Hkac1x+DNRkv0XQiFjo= golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b h1:ZmngSVLe/wycRns9MKikG9OWIEjGcGAkacif7oYQaUY= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 h1:UiNENfZ8gDvpiWw7IpOMQ27spWmThO1RwwdQVbJahJM=
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -1,14 +1,13 @@
package tor package tor
import ( import (
"context"
"crypto/rand" "crypto/rand"
"cwtch.im/tapir"
"cwtch.im/tapir/primitives"
"encoding/base64" "encoding/base64"
"errors" "errors"
"git.openprivacy.ca/cwtch.im/tapir" "git.openprivacy.ca/openprivacy/libricochet-go/connectivity"
"git.openprivacy.ca/cwtch.im/tapir/primitives" "git.openprivacy.ca/openprivacy/libricochet-go/log"
"git.openprivacy.ca/openprivacy/connectivity"
"git.openprivacy.ca/openprivacy/log"
"golang.org/x/crypto/ed25519" "golang.org/x/crypto/ed25519"
"sync" "sync"
"time" "time"
@ -16,34 +15,11 @@ import (
// BaseOnionService is a concrete implementation of the service interface over Tor onion services. // BaseOnionService is a concrete implementation of the service interface over Tor onion services.
type BaseOnionService struct { type BaseOnionService struct {
connections sync.Map connections sync.Map
acn connectivity.ACN acn connectivity.ACN
id *primitives.Identity id *primitives.Identity
privateKey ed25519.PrivateKey privateKey ed25519.PrivateKey
ls connectivity.ListenService ls connectivity.ListenService
lock sync.Mutex
port int
shutdownChannel chan bool
}
// Metrics provides a report of useful information about the status of the service e.g. the number of active
// connections
func (s *BaseOnionService) Metrics() tapir.ServiceMetrics {
s.lock.Lock()
defer s.lock.Unlock()
count := 0
s.connections.Range(func(key, value interface{}) bool {
connection := value.(tapir.Connection)
if !connection.IsClosed() {
count++
}
return true
})
return tapir.ServiceMetrics{
ConnectionCount: count,
}
} }
// Init initializes a BaseOnionService with a given private key and identity // Init initializes a BaseOnionService with a given private key and identity
@ -54,176 +30,49 @@ func (s *BaseOnionService) Init(acn connectivity.ACN, sk ed25519.PrivateKey, id
s.acn = acn s.acn = acn
s.id = id s.id = id
s.privateKey = sk s.privateKey = sk
s.port = 9878
// blocking so we can wait on shutdown for closing of this goroutine
s.shutdownChannel = make(chan bool)
go func() {
for s.waitOrTimeout() {
s.GarbageCollect()
}
log.Debugf("closing down garbage collection goroutine")
}()
}
func (s *BaseOnionService) waitOrTimeout() bool {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
select {
case <-s.shutdownChannel:
return false
case <-ctx.Done():
return true
}
}
// SetPort configures the port that the service uses.
func (s *BaseOnionService) SetPort(port int) {
s.port = port
} }
// WaitForCapabilityOrClose blocks until the connection has the given capability or the underlying connection is closed // WaitForCapabilityOrClose blocks until the connection has the given capability or the underlying connection is closed
// (through error or user action) // (through error or user action)
func (s *BaseOnionService) WaitForCapabilityOrClose(cid string, name tapir.Capability) (tapir.Connection, error) { func (s *BaseOnionService) WaitForCapabilityOrClose(cid string, name tapir.Capability) (tapir.Connection, error) {
attempts := 0 conn, err := s.GetConnection(cid)
for { if err == nil {
if attempts > 4 { for {
s.connections.Range(func(key, value interface{}) bool {
connection := value.(tapir.Connection)
if connection.Hostname() == cid {
if !connection.IsClosed() {
connection.Close()
s.connections.Delete(key)
}
}
return true
})
log.Debugf("WaitForCapabilityOrClose attempts exceeded for %v, all connections closed", cid)
return nil, errors.New("failed to acquire capability after multiple attempts, forcibly closing all connections with the peer")
}
if attempts > 0 {
// Allow connections to be torn down / closed before checking again
// There is no point in busy looping...
time.Sleep(time.Second * time.Duration(attempts))
}
// Increment Attempts
attempts++
log.Debugf("Lookup up a connection %v...", cid)
// Lookup the connection...
conn, err := s.GetConnection(cid)
// If there are no active connections then return an error...
if conn == nil {
log.Debugf("no active connection found for %v", cid)
return nil, err
}
if err == nil {
// if we have only one connection and it has the desired capability then return the connection with
// no error...
if conn.HasCapability(name) { if conn.HasCapability(name) {
return conn, nil return conn, nil
} }
if conn.IsClosed() {
log.Debugf("Found 1 connections for %v, but it lacks the desired capability %v", cid, name) return nil, errors.New("connection is closed")
continue
}
// If We have 2 connections for the same hostname...
if err != nil {
log.Debugf("found duplicate connections for %v <-> %v %v", s.id.Hostname(), cid, err)
inboundCount := 0
// By convention the lowest lexicographical hostname purges all their outbounds to the higher
// hostname
// Which should only leave a single connection remaining (as we dedupe on connect too)
// This does allow people to attempt to guarantee being the outbound connection but due to the bidirectional
// authentication this should never result in an advantage in the protocol.
// Close all outbound connections to connection
s.connections.Range(func(key, value interface{}) bool {
connection := value.(tapir.Connection)
if connection.Hostname() == cid {
if !connection.IsClosed() && connection.IsOutbound() && s.id.Hostname() < cid {
connection.Close()
s.connections.Delete(key)
}
if !connection.IsClosed() && !connection.IsOutbound() {
inboundCount++
}
}
return true
})
// If we have more than 1 inbound count then forcibly close all connections...
// This shouldn't happen honestly, but if it does then it can cause an infinite check here
if inboundCount > 1 {
s.connections.Range(func(key, value interface{}) bool {
connection := value.(tapir.Connection)
if connection.Hostname() == cid {
if !connection.IsClosed() {
connection.Close()
s.connections.Delete(key)
}
}
return true
})
return nil, errors.New("multiple inbound connections found and closed; the only resolution to this is to close them all and try connecting again")
} }
time.Sleep(time.Millisecond * 200)
} }
} }
return nil, err
} }
// GetConnection returns a connection for a given hostname. // GetConnection returns a connection for a given hostname.
func (s *BaseOnionService) GetConnection(hostname string) (tapir.Connection, error) { func (s *BaseOnionService) GetConnection(hostname string) (tapir.Connection, error) {
conn := make([]tapir.Connection, 0) var conn tapir.Connection
s.connections.Range(func(key, value interface{}) bool { s.connections.Range(func(key, value interface{}) bool {
connection := value.(tapir.Connection) connection := value.(tapir.Connection)
if connection.Hostname() == hostname { if connection.Hostname() == hostname {
if !connection.IsClosed() { if !connection.IsClosed() {
conn = append(conn, connection) conn = connection
} else { return false
// Delete this Closed Connection
s.connections.Delete(key)
} }
} }
return true return true
}) })
if conn == nil {
if len(conn) == 0 {
return nil, errors.New("no connection found") return nil, errors.New("no connection found")
} }
if len(conn) > 1 { return conn, nil
// If there are multiple connections we return the first one but also notify the user (this should be a
// temporary edgecase (see Connect))
return conn[0], errors.New("multiple connections found")
}
return conn[0], nil
}
// GarbageCollect iterates through the connection pool and cleans up any connections that are closed
// that haven't been removed from the map.
func (s *BaseOnionService) GarbageCollect() {
log.Debugf("running garbage collection...")
s.connections.Range(func(key, value interface{}) bool {
connection := value.(tapir.Connection)
if connection.IsClosed() {
// Delete this Closed Connection
s.connections.Delete(key)
}
return true
})
} }
// Connect initializes a new outbound connection to the given peer, using the defined Application // Connect initializes a new outbound connection to the given peer, using the defined Application
func (s *BaseOnionService) Connect(hostname string, app tapir.Application) (bool, error) { func (s *BaseOnionService) Connect(hostname string, app tapir.Application) (bool, error) {
currconn, _ := s.GetConnection(hostname) _, err := s.GetConnection(hostname)
if err == nil {
// We already have a connection
if currconn != nil {
// Note: This check is not 100% reliable. And we may end up with two connections between peers // Note: This check is not 100% reliable. And we may end up with two connections between peers
// This can happen when a client connects to a server as the server is connecting to the client // This can happen when a client connects to a server as the server is connecting to the client
// Because at the start of the connection the server cannot derive the true hostname of the client until it // Because at the start of the connection the server cannot derive the true hostname of the client until it
@ -231,7 +80,6 @@ func (s *BaseOnionService) Connect(hostname string, app tapir.Application) (bool
// We mitigate this by performing multiple checks when Connect'ing // We mitigate this by performing multiple checks when Connect'ing
return true, errors.New("already connected to " + hostname) return true, errors.New("already connected to " + hostname)
} }
// connects to a remote server // connects to a remote server
// spins off to a connection struct // spins off to a connection struct
log.Debugf("Connecting to %v", hostname) log.Debugf("Connecting to %v", hostname)
@ -242,14 +90,14 @@ func (s *BaseOnionService) Connect(hostname string, app tapir.Application) (bool
// Second check. If we didn't catch a double connection attempt before the Open we *should* catch it now because // Second check. If we didn't catch a double connection attempt before the Open we *should* catch it now because
// the auth protocol is quick and Open over onion connections can take some time. // the auth protocol is quick and Open over onion connections can take some time.
// Again this isn't 100% reliable. // Again this isn't 100% reliable.
tconn, _ := s.GetConnection(hostname) _, err := s.GetConnection(hostname)
if tconn != nil { if err == nil {
conn.Close() conn.Close()
return true, errors.New("already connected to " + hostname) return true, errors.New("already connected to " + hostname)
} }
log.Debugf("Connected to %v [%v]", hostname, connectionID) log.Debugf("Connected to %v [%v]", hostname, connectionID)
s.connections.Store(connectionID, tapir.NewConnection(s, s.id, hostname, true, conn, app.NewInstance())) s.connections.Store(connectionID, tapir.NewConnection(s.id, hostname, true, conn, app.NewInstance()))
return true, nil return true, nil
} }
log.Debugf("Error connecting to %v %v", hostname, err) log.Debugf("Error connecting to %v %v", hostname, err)
@ -268,19 +116,16 @@ func (s *BaseOnionService) getNewConnectionID() string {
func (s *BaseOnionService) Listen(app tapir.Application) error { func (s *BaseOnionService) Listen(app tapir.Application) error {
// accepts a new connection // accepts a new connection
// spins off to a connection struct // spins off to a connection struct
s.lock.Lock() ls, err := s.acn.Listen(s.privateKey, 9878)
ls, err := s.acn.Listen(s.privateKey, s.port)
s.ls = ls s.ls = ls
s.lock.Unlock() log.Debugf("Starting a service on %v ", ls.AddressFull())
if err == nil { if err == nil {
log.Debugf("Starting a service on %v ", s.ls.AddressFull())
for { for {
conn, err := s.ls.Accept() conn, err := s.ls.Accept()
if err == nil { if err == nil {
tempHostname := s.getNewConnectionID() tempHostname := s.getNewConnectionID()
log.Debugf("Accepted connection from %v", tempHostname) log.Debugf("Accepted connection from %v", tempHostname)
s.connections.Store(tempHostname, tapir.NewConnection(s, s.id, tempHostname, false, conn, app.NewInstance())) s.connections.Store(tempHostname, tapir.NewConnection(s.id, tempHostname, false, conn, app.NewInstance()))
} else { } else {
log.Debugf("Error accepting connection %v", err) log.Debugf("Error accepting connection %v", err)
return err return err
@ -293,33 +138,10 @@ func (s *BaseOnionService) Listen(app tapir.Application) error {
// Shutdown closes the service and ensures that any connections are closed. // Shutdown closes the service and ensures that any connections are closed.
func (s *BaseOnionService) Shutdown() { func (s *BaseOnionService) Shutdown() {
s.lock.Lock() s.ls.Close()
defer s.lock.Unlock()
if s.ls != nil {
s.ls.Close()
}
// close all existing connections manually
s.connections.Range(func(key, value interface{}) bool { s.connections.Range(func(key, value interface{}) bool {
connection := value.(tapir.Connection) connection := value.(tapir.Connection)
connection.Close() connection.Close()
return true return true
}) })
// wait for the return of our garbage collection goroutine
s.shutdownChannel <- true
}
// Broadcast sends a message to all connections who possess the given capability
func (s *BaseOnionService) Broadcast(message []byte, capability tapir.Capability) error {
s.lock.Lock()
defer s.lock.Unlock()
s.connections.Range(func(key, value interface{}) bool {
connection := value.(tapir.Connection)
if connection.HasCapability(capability) {
connection.Send(message)
}
return true
})
return nil
} }

View File

@ -2,7 +2,7 @@ package persistence
import ( import (
"encoding/json" "encoding/json"
"git.openprivacy.ca/openprivacy/log" "git.openprivacy.ca/openprivacy/libricochet-go/log"
bolt "go.etcd.io/bbolt" bolt "go.etcd.io/bbolt"
) )

View File

@ -1,18 +1,14 @@
package persistence package persistence
import ( import (
"os"
"testing" "testing"
) )
func TestBoltPersistence_Open(t *testing.T) { func TestBoltPersistence_Open(t *testing.T) {
os.Remove("test.dbgi") var db Service
db := new(BoltPersistence) db = new(BoltPersistence)
db.Open("test.dbgi") db.Open("test.dbgi")
db.Setup([]string{"tokens"}) db.Setup([]string{"tokens"})
// 2020.02: Fails in WSL1 because of a mmap issue.
// https://github.com/microsoft/WSL/issues/4873
// Scheduled to be fixed in the 20h1 Win10 release
db.Persist("tokens", "random_value", true) db.Persist("tokens", "random_value", true)
var exists bool var exists bool

Binary file not shown.

View File

@ -14,15 +14,11 @@ import (
// Alice Ephemeral <-> Bob Ephemeral // Alice Ephemeral <-> Bob Ephemeral
// //
// Through this, a unique session key is derived. The exchange is offline-deniable (in the context of Tapir and Onion Service) // Through this, a unique session key is derived. The exchange is offline-deniable (in the context of Tapir and Onion Service)
func Perform3DH(longtermIdentity *Identity, ephemeralIdentity *Identity, remoteLongTermPublicKey ed25519.PublicKey, remoteEphemeralPublicKey ed25519.PublicKey, outbound bool) ([32]byte, error) { func Perform3DH(longtermIdentity *Identity, ephemeralIdentity *Identity, remoteLongTermPublicKey ed25519.PublicKey, remoteEphemeralPublicKey ed25519.PublicKey, outbound bool) [32]byte {
// 3DH Handshake // 3DH Handshake
l2e, err1 := longtermIdentity.EDH(remoteEphemeralPublicKey) l2e := longtermIdentity.EDH(remoteEphemeralPublicKey)
e2l, err2 := ephemeralIdentity.EDH(remoteLongTermPublicKey) e2l := ephemeralIdentity.EDH(remoteLongTermPublicKey)
e2e, err3 := ephemeralIdentity.EDH(remoteEphemeralPublicKey) e2e := ephemeralIdentity.EDH(remoteEphemeralPublicKey)
if err1 != nil || err2 != nil || err3 != nil {
return [32]byte{}, err1
}
// We need to define an order for the result concatenation so that both sides derive the same key. // We need to define an order for the result concatenation so that both sides derive the same key.
var result [96]byte var result [96]byte
@ -35,5 +31,5 @@ func Perform3DH(longtermIdentity *Identity, ephemeralIdentity *Identity, remoteL
copy(result[32:64], l2e) copy(result[32:64], l2e)
copy(result[64:96], e2e) copy(result[64:96], e2e)
} }
return sha3.Sum256(result[:]), nil return sha3.Sum256(result[:])
} }

View File

@ -3,12 +3,12 @@ package auditable
// WARNING NOTE: This is a sketch implementation, Not suitable for production use. The real auditable store is still being designed. // WARNING NOTE: This is a sketch implementation, Not suitable for production use. The real auditable store is still being designed.
import ( import (
"cwtch.im/tapir/persistence"
"cwtch.im/tapir/primitives"
"cwtch.im/tapir/primitives/core"
"encoding/base64" "encoding/base64"
"errors" "errors"
"git.openprivacy.ca/cwtch.im/tapir/persistence" "git.openprivacy.ca/openprivacy/libricochet-go/log"
"git.openprivacy.ca/cwtch.im/tapir/primitives"
"git.openprivacy.ca/cwtch.im/tapir/primitives/core"
"git.openprivacy.ca/openprivacy/log"
"golang.org/x/crypto/ed25519" "golang.org/x/crypto/ed25519"
"sync" "sync"
) )
@ -25,6 +25,7 @@ type State struct {
Messages []Message Messages []Message
} }
//
const ( const (
auditableDataStoreProtocol = "auditable-data-store" auditableDataStoreProtocol = "auditable-data-store"
newMessage = "new-message" newMessage = "new-message"
@ -136,7 +137,7 @@ func (as *Store) AppendState(state State) error {
// verify that our state matches the servers signed state // verify that our state matches the servers signed state
// this is *not* a security check, as a rogue server can simply sign any state // this is *not* a security check, as a rogue server can simply sign any state
// however committing to a state allows us to build fraud proofs for malicious servers later on. // however committing to a state allows us to build fraud proofs for malicious servers later on.
if !ed25519.Verify(as.identity.PublicKey(), as.LatestCommit, state.SignedProof) { if ed25519.Verify(as.identity.PublicKey(), as.LatestCommit, state.SignedProof) == false {
return errors.New("state is not consistent, the server is malicious") return errors.New("state is not consistent, the server is malicious")
} }
return nil return nil
@ -159,7 +160,7 @@ func (as *Store) MergeState(state State) error {
// and not the cause (which could be reordered messages, dropped messages, additional messages or any combination) // and not the cause (which could be reordered messages, dropped messages, additional messages or any combination)
func (as *Store) VerifyFraudProof(fraudCommit []byte, signedFraudProof SignedProof, key ed25519.PublicKey) (bool, error) { func (as *Store) VerifyFraudProof(fraudCommit []byte, signedFraudProof SignedProof, key ed25519.PublicKey) (bool, error) {
if !ed25519.Verify(key, fraudCommit, signedFraudProof) { if ed25519.Verify(key, fraudCommit, signedFraudProof) == false {
// This could happen due to misuse of this function (trying to verify a proof with the wrong public key) // This could happen due to misuse of this function (trying to verify a proof with the wrong public key)
// This could happen if the server lies to us and submits a fake state proof, however we cannot use this to // This could happen if the server lies to us and submits a fake state proof, however we cannot use this to
// prove that the server is acting maliciously // prove that the server is acting maliciously

View File

@ -1,10 +1,10 @@
package auditable package auditable
import ( import (
"cwtch.im/tapir/persistence"
"cwtch.im/tapir/primitives"
"fmt" "fmt"
"git.openprivacy.ca/cwtch.im/tapir/persistence" "git.openprivacy.ca/openprivacy/libricochet-go/log"
"git.openprivacy.ca/cwtch.im/tapir/primitives"
"git.openprivacy.ca/openprivacy/log"
"os" "os"
"testing" "testing"
) )

View File

@ -2,7 +2,6 @@ package core
import ( import (
"fmt" "fmt"
"git.openprivacy.ca/openprivacy/log"
"github.com/gtank/merlin" "github.com/gtank/merlin"
ristretto "github.com/gtank/ristretto255" ristretto "github.com/gtank/ristretto255"
"golang.org/x/crypto/sha3" "golang.org/x/crypto/sha3"
@ -12,8 +11,8 @@ import (
// Transcript provides a consistent transcript primitive for our protocols // Transcript provides a consistent transcript primitive for our protocols
// //
// We have the following goals: // We have the following goals:
// - Allow sequential proofs over a common transcript (ensuring a single proof cannot be extracted standalone) // - Allow sequential proofs over a common transcript (ensuring a single proof cannot be extracted standalone)
// - be able to produce a human-readable transcript for auditing. // - be able to produce a human-readable transcript for auditing.
// //
// The design of this API was inspired by Merlin: https://docs.rs/crate/merlin/ // The design of this API was inspired by Merlin: https://docs.rs/crate/merlin/
type Transcript struct { type Transcript struct {
@ -39,7 +38,7 @@ func (t *Transcript) AddToTranscript(label string, b []byte) {
// AddElementToTranscript appends a value to the transcript with the given label // AddElementToTranscript appends a value to the transcript with the given label
// This binds the given data to the label. // This binds the given data to the label.
func (t *Transcript) AddElementToTranscript(label string, element *ristretto.Element) { func (t *Transcript) AddElementToTranscript(label string, element *ristretto.Element) {
t.AddToTranscript(label, element.Bytes()) t.AddToTranscript(label, element.Encode([]byte{}))
} }
// OutputTranscriptToAudit outputs a human-readable copy of the transcript so far. // OutputTranscriptToAudit outputs a human-readable copy of the transcript so far.
@ -68,14 +67,12 @@ type PRNG struct {
} }
// Next returns the next "random" scalar from the PRNG // Next returns the next "random" scalar from the PRNG
func (prng *PRNG) Next(buf []byte, next *ristretto.Scalar) error { func (prng *PRNG) Next() *ristretto.Scalar {
n, err := io.ReadFull(prng.prng, buf) buf := [64]byte{}
if n != 64 || err != nil { io.ReadFull(prng.prng, buf[:])
log.Errorf("could not read prng: %v %v", n, err) next := new(ristretto.Scalar)
return fmt.Errorf("error fetching complete output from prng: %v", err) next.FromUniformBytes(buf[:])
} return next
next.SetUniformBytes(buf)
return nil
} }
// CommitToPRNG commits the label to the transcript and derives a PRNG from the transcript. // CommitToPRNG commits the label to the transcript and derives a PRNG from the transcript.
@ -89,8 +86,7 @@ func (t *Transcript) CommitToPRNG(label string) PRNG {
// CommitToGenerator derives a verifiably random generator from the transcript // CommitToGenerator derives a verifiably random generator from the transcript
func (t *Transcript) CommitToGenerator(label string) *ristretto.Element { func (t *Transcript) CommitToGenerator(label string) *ristretto.Element {
c := t.CommitToTranscript(label) c := t.CommitToTranscript(label)
result, _ := new(ristretto.Element).SetUniformBytes(c) return new(ristretto.Element).FromUniformBytes(c)
return result
} }
// CommitToGenerators derives a set of verifiably random generators from the transcript // CommitToGenerators derives a set of verifiably random generators from the transcript
@ -105,6 +101,6 @@ func (t *Transcript) CommitToGenerators(label string, n int) (generators []*rist
func (t *Transcript) CommitToTranscriptScalar(label string) *ristretto.Scalar { func (t *Transcript) CommitToTranscriptScalar(label string) *ristretto.Scalar {
c := t.CommitToTranscript(label) c := t.CommitToTranscript(label)
s := new(ristretto.Scalar) s := new(ristretto.Scalar)
s.SetUniformBytes(c[:]) s.FromUniformBytes(c[:])
return s return s
} }

View File

@ -11,9 +11,7 @@ func TestNewTranscript(t *testing.T) {
transcript.AddToTranscript("action", []byte("test data")) transcript.AddToTranscript("action", []byte("test data"))
firstAudit := transcript.OutputTranscriptToAudit() if transcript.OutputTranscriptToAudit() != transcript.OutputTranscriptToAudit() {
secondAudit := transcript.OutputTranscriptToAudit()
if firstAudit != secondAudit {
t.Fatalf("Multiple Audit Calls should not impact underlying Transcript") t.Fatalf("Multiple Audit Calls should not impact underlying Transcript")
} }
t.Logf("%v", transcript.OutputTranscriptToAudit()) t.Logf("%v", transcript.OutputTranscriptToAudit())

View File

@ -2,8 +2,7 @@ package primitives
import ( import (
"crypto/rand" "crypto/rand"
"git.openprivacy.ca/cwtch.im/tapir/utils" "git.openprivacy.ca/openprivacy/libricochet-go/utils"
torProvider "git.openprivacy.ca/openprivacy/connectivity/tor"
"golang.org/x/crypto/ed25519" "golang.org/x/crypto/ed25519"
) )
@ -43,14 +42,14 @@ func (i *Identity) PublicKey() ed25519.PublicKey {
} }
// EDH performs a diffie-hellman operation on this identities private key with the given public key. // EDH performs a diffie-hellman operation on this identities private key with the given public key.
func (i *Identity) EDH(key ed25519.PublicKey) ([]byte, error) { func (i *Identity) EDH(key ed25519.PublicKey) []byte {
secret, err := utils.EDH(*i.edpk, key) secret := utils.EDH(*i.edpk, key)
return secret[:], err return secret[:]
} }
// Hostname provides the onion address associated with this Identity. // Hostname provides the onion address associated with this Identity.
func (i *Identity) Hostname() string { func (i *Identity) Hostname() string {
return torProvider.GetTorV3Hostname(*i.edpubk) return utils.GetTorV3Hostname(*i.edpubk)
} }
// Sign produces a signature for a given message attributable to the given identity // Sign produces a signature for a given message attributable to the given identity

View File

@ -1,7 +1,6 @@
package primitives package primitives
import ( import (
"crypto/subtle"
"testing" "testing"
) )
@ -10,28 +9,9 @@ func TestIdentity_EDH(t *testing.T) {
id1, _ := InitializeEphemeralIdentity() id1, _ := InitializeEphemeralIdentity()
id2, _ := InitializeEphemeralIdentity() id2, _ := InitializeEphemeralIdentity()
k1, err1 := id1.EDH(id2.PublicKey()) k1 := id1.EDH(id2.PublicKey())
k2, err2 := id2.EDH(id1.PublicKey()) k2 := id2.EDH(id1.PublicKey())
if err1 == nil && err2 == nil && subtle.ConstantTimeCompare(k1, k2) == 1 { t.Logf("k1: %x\nk2: %x\n", k1, k2)
t.Logf("k1: %x\nk2: %x\n", k1, k2)
} else {
t.Fatalf("The derived keys should be identical")
}
} }
func BenchmarkEDH(b *testing.B) {
id1, _ := InitializeEphemeralIdentity()
id2, _ := InitializeEphemeralIdentity()
for i := 0; i < b.N; i++ {
k1, err1 := id1.EDH(id2.PublicKey())
k2, err2 := id2.EDH(id1.PublicKey())
if err1 == nil && err2 == nil && subtle.ConstantTimeCompare(k1, k2) == 1 {
//b.Logf("k1: %x\nk2: %x\n", k1, k2)
} else {
b.Fatalf("The derived keys should be identical")
}
}
}

View File

@ -2,12 +2,12 @@ package privacypass
import ( import (
"crypto/rand" "crypto/rand"
"git.openprivacy.ca/cwtch.im/tapir/primitives/core" "cwtch.im/tapir/primitives/core"
ristretto "github.com/gtank/ristretto255" ristretto "github.com/gtank/ristretto255"
) )
// DLEQProof encapsulates a Chaum-Pedersen DLEQ Proof // DLEQProof encapsulates a Chaum-Pedersen DLEQ Proof
// gut In Ernest F. Brickell, editor,CRYPTO92,volume 740 ofLNCS, pages 89105. Springer, Heidelberg,August 1993 //gut In Ernest F. Brickell, editor,CRYPTO92,volume 740 ofLNCS, pages 89105. Springer, Heidelberg,August 1993
type DLEQProof struct { type DLEQProof struct {
C *ristretto.Scalar C *ristretto.Scalar
S *ristretto.Scalar S *ristretto.Scalar
@ -16,29 +16,26 @@ type DLEQProof struct {
// DiscreteLogEquivalenceProof constructs a valid DLEQProof for the given parameters and transcript // DiscreteLogEquivalenceProof constructs a valid DLEQProof for the given parameters and transcript
// Given Y = kX & Q = kP // Given Y = kX & Q = kP
// Peggy: t := choose randomly from Zq // Peggy: t := choose randomly from Zq
// // A := tX
// A := tX // B := tP
// B := tP // c := H(transcript(X,Y,P,Q,A,B))
// c := H(transcript(X,Y,P,Q,A,B)) // s := (t + ck) mod q
// s := (t + ck) mod q
// //
// Sends c,s to Vicky // Sends c,s to Vicky
func DiscreteLogEquivalenceProof(k *ristretto.Scalar, X *ristretto.Element, Y *ristretto.Element, P *ristretto.Element, Q *ristretto.Element, transcript *core.Transcript) DLEQProof { func DiscreteLogEquivalenceProof(k *ristretto.Scalar, X *ristretto.Element, Y *ristretto.Element, P *ristretto.Element, Q *ristretto.Element, transcript *core.Transcript) DLEQProof {
private := make([]byte, 64) private := make([]byte, 64)
rand.Read(private) rand.Read(private)
t, err := new(ristretto.Scalar).SetUniformBytes(private) t := new(ristretto.Scalar)
if err != nil { t.FromUniformBytes(private)
return DLEQProof{ristretto.NewScalar(), ristretto.NewScalar()}
}
A := new(ristretto.Element).ScalarMult(t, X) A := new(ristretto.Element).ScalarMult(t, X)
B := new(ristretto.Element).ScalarMult(t, P) B := new(ristretto.Element).ScalarMult(t, P)
transcript.AddToTranscript(DLEQX, X.Bytes()) transcript.AddToTranscript(DLEQX, X.Encode(nil))
transcript.AddToTranscript(DLEQY, Y.Bytes()) transcript.AddToTranscript(DLEQY, Y.Encode(nil))
transcript.AddToTranscript(DLEQP, P.Bytes()) transcript.AddToTranscript(DLEQP, P.Encode(nil))
transcript.AddToTranscript(DLEQQ, Q.Bytes()) transcript.AddToTranscript(DLEQQ, Q.Encode(nil))
transcript.AddToTranscript(DLEQA, A.Bytes()) transcript.AddToTranscript(DLEQA, A.Encode(nil))
transcript.AddToTranscript(DLEQB, B.Bytes()) transcript.AddToTranscript(DLEQB, B.Encode(nil))
c := transcript.CommitToTranscriptScalar("c") c := transcript.CommitToTranscriptScalar("c")
s := new(ristretto.Scalar).Subtract(t, new(ristretto.Scalar).Multiply(c, k)) s := new(ristretto.Scalar).Subtract(t, new(ristretto.Scalar).Multiply(c, k))
@ -48,14 +45,12 @@ func DiscreteLogEquivalenceProof(k *ristretto.Scalar, X *ristretto.Element, Y *r
// VerifyDiscreteLogEquivalenceProof verifies the DLEQ for the given parameters and transcript // VerifyDiscreteLogEquivalenceProof verifies the DLEQ for the given parameters and transcript
// Given Y = kX & Q = kP and Proof = (c,s) // Given Y = kX & Q = kP and Proof = (c,s)
// Vicky: X' := sX // Vicky: X' := sX
// // Y' := cY
// Y' := cY // P' := sP
// P' := sP // Q' := cQ
// Q' := cQ // A' = X'+Y' == sX + cY ?= sG + ckG == (s+ck)X == tX == A
// A' = X'+Y' == sX + cY ?= sG + ckG == (s+ck)X == tX == A // B' = P'+Q' == sP + cQ ?= sP + ckP == (s+ck)P == tP == B
// B' = P'+Q' == sP + cQ ?= sP + ckP == (s+ck)P == tP == B // c' := H(transcript(X,Y,P,Q,A',B'))
// c' := H(transcript(X,Y,P,Q,A',B'))
//
// Tests c ?= c // Tests c ?= c
func VerifyDiscreteLogEquivalenceProof(dleq DLEQProof, X *ristretto.Element, Y *ristretto.Element, P *ristretto.Element, Q *ristretto.Element, transcript *core.Transcript) bool { func VerifyDiscreteLogEquivalenceProof(dleq DLEQProof, X *ristretto.Element, Y *ristretto.Element, P *ristretto.Element, Q *ristretto.Element, transcript *core.Transcript) bool {
@ -67,12 +62,12 @@ func VerifyDiscreteLogEquivalenceProof(dleq DLEQProof, X *ristretto.Element, Y *
A := new(ristretto.Element).Add(Xs, Yc) A := new(ristretto.Element).Add(Xs, Yc)
B := new(ristretto.Element).Add(Ps, Qc) B := new(ristretto.Element).Add(Ps, Qc)
transcript.AddToTranscript(DLEQX, X.Bytes()) transcript.AddToTranscript(DLEQX, X.Encode(nil))
transcript.AddToTranscript(DLEQY, Y.Bytes()) transcript.AddToTranscript(DLEQY, Y.Encode(nil))
transcript.AddToTranscript(DLEQP, P.Bytes()) transcript.AddToTranscript(DLEQP, P.Encode(nil))
transcript.AddToTranscript(DLEQQ, Q.Bytes()) transcript.AddToTranscript(DLEQQ, Q.Encode(nil))
transcript.AddToTranscript(DLEQA, A.Bytes()) transcript.AddToTranscript(DLEQA, A.Encode(nil))
transcript.AddToTranscript(DLEQB, B.Bytes()) transcript.AddToTranscript(DLEQB, B.Encode(nil))
return transcript.CommitToTranscriptScalar("c").Equal(dleq.C) == 1 return transcript.CommitToTranscriptScalar("c").Equal(dleq.C) == 1
} }

View File

@ -3,10 +3,9 @@ package privacypass
import ( import (
"crypto/hmac" "crypto/hmac"
"crypto/rand" "crypto/rand"
"encoding/json" "cwtch.im/tapir/primitives/core"
"fmt" "fmt"
"git.openprivacy.ca/cwtch.im/tapir/primitives/core" "git.openprivacy.ca/openprivacy/libricochet-go/log"
"git.openprivacy.ca/openprivacy/log"
ristretto "github.com/gtank/ristretto255" ristretto "github.com/gtank/ristretto255"
"golang.org/x/crypto/sha3" "golang.org/x/crypto/sha3"
@ -56,10 +55,10 @@ func (t *Token) GenBlindedToken() BlindedToken {
t.r = new(ristretto.Scalar) t.r = new(ristretto.Scalar)
b := make([]byte, 64) b := make([]byte, 64)
rand.Read(b) rand.Read(b)
t.r.SetUniformBytes(b) t.r.FromUniformBytes(b)
Ht := sha3.Sum512(t.t) Ht := sha3.Sum512(t.t)
T, _ := new(ristretto.Element).SetUniformBytes(Ht[:]) T := new(ristretto.Element).FromUniformBytes(Ht[:])
P := new(ristretto.Element).ScalarMult(t.r, T) P := new(ristretto.Element).ScalarMult(t.r, T)
return BlindedToken{P} return BlindedToken{P}
} }
@ -71,10 +70,9 @@ func (t *Token) unblindSignedToken(token SignedToken) {
// SpendToken binds the token with data and then redeems the token // SpendToken binds the token with data and then redeems the token
func (t *Token) SpendToken(data []byte) SpentToken { func (t *Token) SpendToken(data []byte) SpentToken {
key := sha3.Sum256(append(t.t, t.W.Bytes()...)) key := sha3.Sum256(append(t.t, t.W.Encode(nil)...))
mac := hmac.New(sha3.New512, key[:]) mac := hmac.New(sha3.New512, key[:])
mac.Write(data) return SpentToken{t.t, mac.Sum(data)}
return SpentToken{t.t, mac.Sum(nil)}
} }
// GenerateBlindedTokenBatch generates a batch of blinded tokens (and their unblinded equivalents) // GenerateBlindedTokenBatch generates a batch of blinded tokens (and their unblinded equivalents)
@ -89,25 +87,19 @@ func GenerateBlindedTokenBatch(num int) (tokens []*Token, blindedTokens []Blinde
// verifyBatchProof verifies a given batch proof (see also UnblindSignedTokenBatch) // verifyBatchProof verifies a given batch proof (see also UnblindSignedTokenBatch)
func verifyBatchProof(dleq DLEQProof, Y *ristretto.Element, blindedTokens []BlindedToken, signedTokens []SignedToken, transcript *core.Transcript) bool { func verifyBatchProof(dleq DLEQProof, Y *ristretto.Element, blindedTokens []BlindedToken, signedTokens []SignedToken, transcript *core.Transcript) bool {
transcript.NewProtocol(BatchProofProtocol) transcript.NewProtocol(BatchProofProtocol)
transcript.AddToTranscript(BatchProofX, ristretto.NewGeneratorElement().Bytes()) transcript.AddToTranscript(BatchProofX, new(ristretto.Element).Base().Encode(nil))
transcript.AddToTranscript(BatchProofY, Y.Bytes()) transcript.AddToTranscript(BatchProofY, Y.Encode(nil))
transcript.AddToTranscript(BatchProofPVector, []byte(fmt.Sprintf("%v", blindedTokens))) transcript.AddToTranscript(BatchProofPVector, []byte(fmt.Sprintf("%v", blindedTokens)))
transcript.AddToTranscript(BatchProofQVector, []byte(fmt.Sprintf("%v", signedTokens))) transcript.AddToTranscript(BatchProofQVector, []byte(fmt.Sprintf("%v", signedTokens)))
prng := transcript.CommitToPRNG("w") prng := transcript.CommitToPRNG("w")
M := ristretto.NewIdentityElement() M := new(ristretto.Element).Zero()
Z := ristretto.NewIdentityElement() Z := new(ristretto.Element).Zero()
buf := make([]byte, 64)
c := new(ristretto.Scalar)
for i := range blindedTokens { for i := range blindedTokens {
err := prng.Next(buf, c) c := prng.Next()
if err != nil {
log.Errorf("error verifying batch proof: %v", err)
return false
}
M = new(ristretto.Element).Add(new(ristretto.Element).ScalarMult(c, blindedTokens[i].P), M) M = new(ristretto.Element).Add(new(ristretto.Element).ScalarMult(c, blindedTokens[i].P), M)
Z = new(ristretto.Element).Add(new(ristretto.Element).ScalarMult(c, signedTokens[i].Q), Z) Z = new(ristretto.Element).Add(new(ristretto.Element).ScalarMult(c, signedTokens[i].Q), Z)
} }
return VerifyDiscreteLogEquivalenceProof(dleq, ristretto.NewGeneratorElement(), Y, M, Z, transcript) return VerifyDiscreteLogEquivalenceProof(dleq, new(ristretto.Element).Base(), Y, M, Z, transcript)
} }
// UnblindSignedTokenBatch taking in a set of tokens, their blinded & signed counterparts, a server public key (Y), a DLEQ proof and a transcript // UnblindSignedTokenBatch taking in a set of tokens, their blinded & signed counterparts, a server public key (Y), a DLEQ proof and a transcript
@ -123,16 +115,3 @@ func UnblindSignedTokenBatch(tokens []*Token, blindedTokens []BlindedToken, sign
} }
return true return true
} }
// MarshalJSON - in order to store tokens in a serialized form we need to expose the private, unexported value
// `t`. Note that `r` is not needed to spend the token, and as such we effectively destroy it when we serialize.
// Ideally, go would let us do this with an annotation, alas.
func (t Token) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
T []byte `json:"t"`
W *ristretto.Element
}{
T: t.t,
W: t.W,
})
}

View File

@ -1,10 +1,9 @@
package privacypass package privacypass
import ( import (
"crypto/sha512" "cwtch.im/tapir/persistence"
"git.openprivacy.ca/cwtch.im/tapir/persistence" "cwtch.im/tapir/primitives/core"
"git.openprivacy.ca/cwtch.im/tapir/primitives/core" "git.openprivacy.ca/openprivacy/libricochet-go/log"
"git.openprivacy.ca/openprivacy/log"
"github.com/gtank/ristretto255" "github.com/gtank/ristretto255"
"golang.org/x/crypto/sha3" "golang.org/x/crypto/sha3"
"testing" "testing"
@ -52,16 +51,11 @@ func TestToken_ConstrainToToken(t *testing.T) {
token2 := new(Token) token2 := new(Token)
blindedToken2 := token2.GenBlindedToken() blindedToken2 := token2.GenBlindedToken()
Ht := sha3.Sum512(token.t) Ht := sha3.Sum512(token.t)
T, _ := new(ristretto255.Element).SetUniformBytes(Ht[:]) T := new(ristretto255.Element).FromUniformBytes(Ht[:])
// Constraint forces T = kW to be part of the batch proof // Constraint forces T = kW to be part of the batch proof
// And because the batch proof must prove that *all* inputs share the same key and also checks the servers public key // And because the batch proof must prove that *all* inputs share the same key and also checks the servers public key
// We get a consistency check for almost free. // We get a consistency check for almost free.
signedTokens, err := server.SignBlindedTokenBatchWithConstraint([]BlindedToken{blindedToken2}, token.t, core.NewTranscript("")) signedTokens := server.SignBlindedTokenBatchWithConstraint([]BlindedToken{blindedToken2}, token.t, core.NewTranscript(""))
if err != nil {
t.Fatalf("error signing tokens with constraints")
}
transcript := core.NewTranscript("") transcript := core.NewTranscript("")
// NOTE: For this to work token.t and token.W need to be obtain by the client from known source e.g. a public message board. // NOTE: For this to work token.t and token.W need to be obtain by the client from known source e.g. a public message board.
@ -73,20 +67,14 @@ func TestGenerateBlindedTokenBatch(t *testing.T) {
log.SetLevel(log.LevelDebug) log.SetLevel(log.LevelDebug)
db := new(persistence.BoltPersistence) db := new(persistence.BoltPersistence)
db.Open("tokens.db") db.Open("tokens.db")
defer db.Close()
fakeRand := sha512.Sum512([]byte{}) server := NewTokenServerFromStore(db)
k, _ := ristretto255.NewScalar().SetUniformBytes(fakeRand[:])
server := NewTokenServerFromStore(k, db)
defer server.Close()
clientTranscript := core.NewTranscript("privacyPass") clientTranscript := core.NewTranscript("privacyPass")
serverTranscript := core.NewTranscript("privacyPass") serverTranscript := core.NewTranscript("privacyPass")
tokens, blindedTokens := GenerateBlindedTokenBatch(10) tokens, blindedTokens := GenerateBlindedTokenBatch(10)
batchProof, err := server.SignBlindedTokenBatch(blindedTokens, serverTranscript) batchProof := server.SignBlindedTokenBatch(blindedTokens, serverTranscript)
if err != nil {
t.Fatalf("error constructing signed/blinded token batch: %v", err)
}
verified := UnblindSignedTokenBatch(tokens, blindedTokens, batchProof.SignedTokens, server.Y, batchProof.Proof, clientTranscript) verified := UnblindSignedTokenBatch(tokens, blindedTokens, batchProof.SignedTokens, server.Y, batchProof.Proof, clientTranscript)
@ -110,5 +98,4 @@ func TestGenerateBlindedTokenBatch(t *testing.T) {
if verified { if verified {
t.Errorf("Something went wrong, the proof passed with wrong transcript: %s", wrongTranscript.OutputTranscriptToAudit()) t.Errorf("Something went wrong, the proof passed with wrong transcript: %s", wrongTranscript.OutputTranscriptToAudit())
} }
} }

View File

@ -3,11 +3,10 @@ package privacypass
import ( import (
"crypto/hmac" "crypto/hmac"
"crypto/rand" "crypto/rand"
"cwtch.im/tapir/persistence"
"cwtch.im/tapir/primitives/core"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"git.openprivacy.ca/cwtch.im/tapir/persistence"
"git.openprivacy.ca/cwtch.im/tapir/primitives/core"
"git.openprivacy.ca/openprivacy/log"
ristretto "github.com/gtank/ristretto255" ristretto "github.com/gtank/ristretto255"
"golang.org/x/crypto/sha3" "golang.org/x/crypto/sha3"
"sync" "sync"
@ -29,6 +28,7 @@ type SignedBatchWithProof struct {
} }
const tokenBucket = "tokens" const tokenBucket = "tokens"
const keyBucket = "keys"
// NewTokenServer generates a new TokenServer (used mostly for testing with ephemeral instances) // NewTokenServer generates a new TokenServer (used mostly for testing with ephemeral instances)
func NewTokenServer() *TokenServer { func NewTokenServer() *TokenServer {
@ -39,30 +39,34 @@ func NewTokenServer() *TokenServer {
// unable to generate secure random numbers // unable to generate secure random numbers
panic("unable to generate secure random numbers") panic("unable to generate secure random numbers")
} }
k.SetUniformBytes(b) k.FromUniformBytes(b)
return &TokenServer{k, new(ristretto.Element).ScalarBaseMult(k), make(map[string]bool), nil, sync.Mutex{}} return &TokenServer{k, new(ristretto.Element).ScalarBaseMult(k), make(map[string]bool), nil, sync.Mutex{}}
} }
// NewTokenServerFromStore generates a new TokenServer backed by a persistence service. // NewTokenServerFromStore generates a new TokenServer backed by a persistence service.
func NewTokenServerFromStore(k *ristretto.Scalar, persistenceService persistence.Service) *TokenServer { func NewTokenServerFromStore(persistenceService persistence.Service) *TokenServer {
tokenServer := NewTokenServer() tokenServer := NewTokenServer()
persistenceService.Setup([]string{tokenBucket}) persistenceService.Setup([]string{tokenBucket})
persistenceService.Setup([]string{keyBucket})
exists, err := persistenceService.Check(keyBucket, "k")
if err != nil {
panic(err)
}
// if we don't have a stored k then save the one we have generated
// otherwise use the k we have stored
if !exists {
persistenceService.Persist(keyBucket, "k", tokenServer.k)
} else {
persistenceService.Load(keyBucket, "k", tokenServer.k)
// recalculate public key from k // recalculate public key from stored k
tokenServer.k = k tokenServer.Y = new(ristretto.Element).ScalarBaseMult(tokenServer.k)
tokenServer.Y = new(ristretto.Element).ScalarBaseMult(tokenServer.k) }
tokenServer.persistanceService = persistenceService tokenServer.persistanceService = persistenceService
return tokenServer return tokenServer
} }
// Close ensures that the database is properly closed...
func (ts *TokenServer) Close() {
ts.mutex.Lock()
defer ts.mutex.Unlock()
ts.persistanceService.Close()
}
// SignBlindedToken calculates kP for the given BlindedToken P // SignBlindedToken calculates kP for the given BlindedToken P
func (ts *TokenServer) SignBlindedToken(bt BlindedToken) SignedToken { func (ts *TokenServer) SignBlindedToken(bt BlindedToken) SignedToken {
Q := new(ristretto.Element).ScalarMult(ts.k, bt.P) Q := new(ristretto.Element).ScalarMult(ts.k, bt.P)
@ -70,68 +74,47 @@ func (ts *TokenServer) SignBlindedToken(bt BlindedToken) SignedToken {
} }
// SignBlindedTokenBatch signs a batch of blinded tokens under a given transcript // SignBlindedTokenBatch signs a batch of blinded tokens under a given transcript
func (ts *TokenServer) SignBlindedTokenBatch(blindedTokens []BlindedToken, transcript *core.Transcript) (*SignedBatchWithProof, error) { func (ts *TokenServer) SignBlindedTokenBatch(blindedTokens []BlindedToken, transcript *core.Transcript) SignedBatchWithProof {
var signedTokens []SignedToken var signedTokens []SignedToken
for _, bt := range blindedTokens { for _, bt := range blindedTokens {
signedTokens = append(signedTokens, ts.SignBlindedToken(bt)) signedTokens = append(signedTokens, ts.SignBlindedToken(bt))
} }
return SignedBatchWithProof{signedTokens, ts.constructBatchProof(blindedTokens, signedTokens, transcript)}
proof, err := ts.constructBatchProof(blindedTokens, signedTokens, transcript)
if err != nil {
return nil, err
}
signedProof := SignedBatchWithProof{signedTokens, *proof}
return &signedProof, nil
} }
// SignBlindedTokenBatchWithConstraint signs a batch of blinded tokens under a given transcript given a constraint that the tokens must be signed // SignBlindedTokenBatchWithConstraint signs a batch of blinded tokens under a given transcript given a constraint that the tokens must be signed
// by the same public key as an existing token // by the same public key as an existing token
func (ts *TokenServer) SignBlindedTokenBatchWithConstraint(blindedTokens []BlindedToken, constraintToken []byte, transcript *core.Transcript) (*SignedBatchWithProof, error) { func (ts *TokenServer) SignBlindedTokenBatchWithConstraint(blindedTokens []BlindedToken, constraintToken []byte, transcript *core.Transcript) SignedBatchWithProof {
var signedTokens []SignedToken var signedTokens []SignedToken
for _, bt := range blindedTokens { for _, bt := range blindedTokens {
signedTokens = append(signedTokens, ts.SignBlindedToken(bt)) signedTokens = append(signedTokens, ts.SignBlindedToken(bt))
} }
Ht := sha3.Sum512(constraintToken) Ht := sha3.Sum512(constraintToken)
T, err := new(ristretto.Element).SetUniformBytes(Ht[:]) T := new(ristretto.Element).FromUniformBytes(Ht[:])
if err != nil {
return nil, err
}
// W == kT // W == kT
W := new(ristretto.Element).ScalarMult(ts.k, T) W := new(ristretto.Element).ScalarMult(ts.k, T)
blindedTokens = append(blindedTokens, BlindedToken{P: T}) blindedTokens = append(blindedTokens, BlindedToken{P: T})
proof, err := ts.constructBatchProof(blindedTokens, append(signedTokens, SignedToken{Q: W}), transcript) return SignedBatchWithProof{signedTokens, ts.constructBatchProof(blindedTokens, append(signedTokens, SignedToken{Q: W}), transcript)}
if err != nil {
return nil, err
}
signedProof := SignedBatchWithProof{signedTokens, *proof}
return &signedProof, nil
} }
// constructBatchProof construct a batch proof that all the signed tokens have been signed correctly // constructBatchProof construct a batch proof that all the signed tokens have been signed correctly
func (ts *TokenServer) constructBatchProof(blindedTokens []BlindedToken, signedTokens []SignedToken, transcript *core.Transcript) (*DLEQProof, error) { func (ts *TokenServer) constructBatchProof(blindedTokens []BlindedToken, signedTokens []SignedToken, transcript *core.Transcript) DLEQProof {
transcript.NewProtocol(BatchProofProtocol) transcript.NewProtocol(BatchProofProtocol)
transcript.AddToTranscript(BatchProofX, ristretto.NewGeneratorElement().Bytes()) transcript.AddToTranscript(BatchProofX, new(ristretto.Element).Base().Encode(nil))
transcript.AddToTranscript(BatchProofY, ts.Y.Bytes()) transcript.AddToTranscript(BatchProofY, ts.Y.Encode(nil))
transcript.AddToTranscript(BatchProofPVector, []byte(fmt.Sprintf("%v", blindedTokens))) transcript.AddToTranscript(BatchProofPVector, []byte(fmt.Sprintf("%v", blindedTokens)))
transcript.AddToTranscript(BatchProofQVector, []byte(fmt.Sprintf("%v", signedTokens))) transcript.AddToTranscript(BatchProofQVector, []byte(fmt.Sprintf("%v", signedTokens)))
prng := transcript.CommitToPRNG("w") prng := transcript.CommitToPRNG("w")
M := ristretto.NewIdentityElement() M := new(ristretto.Element).Zero()
Z := ristretto.NewIdentityElement() Z := new(ristretto.Element).Zero()
buf := make([]byte, 64)
c := new(ristretto.Scalar)
for i := range blindedTokens { for i := range blindedTokens {
err := prng.Next(buf, c) c := prng.Next()
if err != nil {
log.Errorf("error constructing batch proof: %v", err)
return nil, err
}
M = new(ristretto.Element).Add(new(ristretto.Element).ScalarMult(c, blindedTokens[i].P), M) M = new(ristretto.Element).Add(new(ristretto.Element).ScalarMult(c, blindedTokens[i].P), M)
Z = new(ristretto.Element).Add(new(ristretto.Element).ScalarMult(c, signedTokens[i].Q), Z) Z = new(ristretto.Element).Add(new(ristretto.Element).ScalarMult(c, signedTokens[i].Q), Z)
} }
proof := DiscreteLogEquivalenceProof(ts.k, ristretto.NewGeneratorElement(), ts.Y, M, Z, transcript) return DiscreteLogEquivalenceProof(ts.k, new(ristretto.Element).Base(), ts.Y, M, Z, transcript)
return &proof, nil
} }
// SpendToken returns true a SpentToken is valid and has never been spent before, false otherwise. // SpendToken returns true a SpentToken is valid and has never been spent before, false otherwise.
@ -144,22 +127,18 @@ func (ts *TokenServer) SpendToken(token SpentToken, data []byte) error {
} }
} else { } else {
spent, err := ts.persistanceService.Check(tokenBucket, hex.EncodeToString(token.T)) spent, err := ts.persistanceService.Check(tokenBucket, hex.EncodeToString(token.T))
if err != nil || spent { if err != nil || spent == true {
return fmt.Errorf("token: %v has already been spent", token) return fmt.Errorf("token: %v has already been spent", token)
} }
} }
Ht := sha3.Sum512(token.T) Ht := sha3.Sum512(token.T)
T, err := new(ristretto.Element).SetUniformBytes(Ht[:]) T := new(ristretto.Element).FromUniformBytes(Ht[:])
if err != nil {
return err
}
W := new(ristretto.Element).ScalarMult(ts.k, T) W := new(ristretto.Element).ScalarMult(ts.k, T)
key := sha3.Sum256(append(token.T, W.Bytes()...)) key := sha3.Sum256(append(token.T, W.Encode(nil)...))
mac := hmac.New(sha3.New512, key[:]) mac := hmac.New(sha3.New512, key[:])
mac.Write(data) computedMAC := mac.Sum(data)
computedMAC := mac.Sum(nil)
result := hmac.Equal(token.MAC, computedMAC) result := hmac.Equal(token.MAC, computedMAC)
if result { if result == true {
if ts.persistanceService == nil { if ts.persistanceService == nil {
ts.seen[hex.EncodeToString(token.T)] = true ts.seen[hex.EncodeToString(token.T)] = true
} else { } else {

View File

@ -2,30 +2,22 @@ package tapir
import ( import (
"crypto/rand" "crypto/rand"
"cwtch.im/tapir/primitives"
"encoding/binary" "encoding/binary"
"errors" "git.openprivacy.ca/openprivacy/libricochet-go/connectivity"
"git.openprivacy.ca/cwtch.im/tapir/primitives" "git.openprivacy.ca/openprivacy/libricochet-go/log"
"git.openprivacy.ca/openprivacy/connectivity"
"git.openprivacy.ca/openprivacy/log"
"golang.org/x/crypto/ed25519" "golang.org/x/crypto/ed25519"
"golang.org/x/crypto/nacl/secretbox" "golang.org/x/crypto/nacl/secretbox"
"io" "io"
"sync" "sync"
) )
// ServiceMetrics outlines higher level information about the service e.g. counts of connections
type ServiceMetrics struct {
ConnectionCount int
}
// Service defines the interface for a Tapir Service // Service defines the interface for a Tapir Service
type Service interface { type Service interface {
Init(acn connectivity.ACN, privateKey ed25519.PrivateKey, identity *primitives.Identity) Init(acn connectivity.ACN, privateKey ed25519.PrivateKey, identity *primitives.Identity)
Connect(hostname string, application Application) (bool, error) Connect(hostname string, application Application) (bool, error)
Listen(application Application) error Listen(application Application) error
GetConnection(connectionID string) (Connection, error) GetConnection(connectionID string) (Connection, error)
Metrics() ServiceMetrics
Broadcast(message []byte, capability Capability) error
WaitForCapabilityOrClose(connectionID string, capability Capability) (Connection, error) WaitForCapabilityOrClose(connectionID string, capability Capability) (Connection, error)
Shutdown() Shutdown()
} }
@ -40,12 +32,11 @@ type Connection interface {
HasCapability(name Capability) bool HasCapability(name Capability) bool
SetCapability(name Capability) SetCapability(name Capability)
SetEncryptionKey(key [32]byte) SetEncryptionKey(key [32]byte)
Send(message []byte) error Send(message []byte)
Close() Close()
App() Application App() Application
SetApp(application Application) SetApp(application Application)
IsClosed() bool IsClosed() bool
Broadcast(message []byte, capability Capability) error
} }
// Connection defines a Tapir Connection // Connection defines a Tapir Connection
@ -61,21 +52,17 @@ type connection struct {
closed bool closed bool
MaxLength int MaxLength int
lock sync.Mutex lock sync.Mutex
service Service
expectBuffer []byte
} }
// NewConnection creates a new Connection // NewConnection creates a new Connection
func NewConnection(service Service, id *primitives.Identity, hostname string, outbound bool, conn io.ReadWriteCloser, app Application) Connection { func NewConnection(id *primitives.Identity, hostname string, outbound bool, conn io.ReadWriteCloser, app Application) Connection {
connection := new(connection) connection := new(connection)
connection.hostname = hostname connection.hostname = hostname
connection.conn = conn connection.conn = conn
connection.app = app connection.app = app
connection.identity = id connection.identity = id
connection.outbound = outbound connection.outbound = outbound
connection.MaxLength = 8192 connection.MaxLength = 1024
connection.service = service
connection.expectBuffer = make([]byte, 8192)
go connection.app.Init(connection) go connection.app.Init(connection)
return connection return connection
} }
@ -144,45 +131,42 @@ func (c *connection) HasCapability(name Capability) bool {
func (c *connection) Close() { func (c *connection) Close() {
c.lock.Lock() c.lock.Lock()
defer c.lock.Unlock() defer c.lock.Unlock()
c.closeInner()
}
func (c *connection) closeInner() {
c.closed = true c.closed = true
c.conn.Close() c.conn.Close()
} }
// Expect blocks and reads a single Tapir packet , from the connection. // Expect blocks and reads a single Tapir packet , from the connection.
func (c *connection) Expect() []byte { func (c *connection) Expect() []byte {
// Multiple goroutines may invoke methods on a Conn simultaneously. buffer := make([]byte, c.MaxLength)
// As such we don't need to mutex around closed. n, err := io.ReadFull(c.conn, buffer)
n, err := io.ReadFull(c.conn, c.expectBuffer)
if n != c.MaxLength || err != nil { if n != c.MaxLength || err != nil {
log.Debugf("[%v -> %v] Wire Error Reading, Read %d bytes, Error: %v", c.hostname, c.identity.Hostname(), n, err) log.Errorf("[%v -> %v] Wire Error Reading, Read %d bytes, Error: %v", c.hostname, c.identity.Hostname(), n, err)
c.Close() // use the full close function which acquires a lock for the connection state... c.conn.Close()
c.closed = true
return []byte{} return []byte{}
} }
c.lock.Lock() c.lock.Lock()
defer c.lock.Unlock() defer c.lock.Unlock()
if c.encrypted { if c.encrypted {
var decryptNonce [24]byte var decryptNonce [24]byte
copy(decryptNonce[:], c.expectBuffer[:24]) copy(decryptNonce[:], buffer[:24])
decrypted, ok := secretbox.Open(nil, c.expectBuffer[24:], &decryptNonce, &c.key) decrypted, ok := secretbox.Open(nil, buffer[24:], &decryptNonce, &c.key)
if ok { if ok {
copy(c.expectBuffer, decrypted) copy(buffer, decrypted)
} else { } else {
log.Errorf("[%v -> %v] Error Decrypting Message On Wire", c.hostname, c.identity.Hostname()) log.Errorf("[%v -> %v] Error Decrypting Message On Wire", c.hostname, c.identity.Hostname())
c.closeInner() c.conn.Close()
c.closed = true
return []byte{} return []byte{}
} }
} }
length, _ := binary.Uvarint(c.expectBuffer[0:2]) length, _ := binary.Uvarint(buffer[0:2])
if length+2 >= uint64(c.MaxLength) { if length+2 >= uint64(c.MaxLength) {
return []byte{} return []byte{}
} }
//cplog.Debugf("[%v -> %v] Wire Receive: (%d) %x", c.hostname, c.ID.Hostname(), len, buffer) //cplog.Debugf("[%v -> %v] Wire Receive: (%d) %x", c.hostname, c.ID.Hostname(), len, buffer)
return c.expectBuffer[2 : length+2] return buffer[2 : length+2]
} }
// SetEncryptionKey turns on application-level encryption on the connection using the given key. // SetEncryptionKey turns on application-level encryption on the connection using the given key.
@ -194,13 +178,7 @@ func (c *connection) SetEncryptionKey(key [32]byte) {
} }
// Send writes a given message to a Tapir packet (of 1024 bytes in length). // Send writes a given message to a Tapir packet (of 1024 bytes in length).
func (c *connection) Send(message []byte) error { func (c *connection) Send(message []byte) {
// We can only encode messages up to maxLength
if len(message) >= c.MaxLength {
log.Errorf("attempting to send a message that is too big")
return errors.New("message too long")
}
buffer := make([]byte, c.MaxLength) buffer := make([]byte, c.MaxLength)
binary.PutUvarint(buffer[0:2], uint64(len(message))) binary.PutUvarint(buffer[0:2], uint64(len(message)))
@ -212,8 +190,8 @@ func (c *connection) Send(message []byte) error {
var nonce [24]byte var nonce [24]byte
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil { if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
log.Errorf("Could not read sufficient randomness %v. Closing connection", err) log.Errorf("Could not read sufficient randomness %v. Closing connection", err)
c.closeInner() c.conn.Close()
return errors.New("could not read random") c.closed = true
} }
// MaxLength - 40 = MaxLength - 24 nonce bytes and 16 auth tag. // MaxLength - 40 = MaxLength - 24 nonce bytes and 16 auth tag.
encrypted := secretbox.Seal(nonce[:], buffer[0:c.MaxLength-40], &nonce, &c.key) encrypted := secretbox.Seal(nonce[:], buffer[0:c.MaxLength-40], &nonce, &c.key)
@ -222,12 +200,7 @@ func (c *connection) Send(message []byte) error {
log.Debugf("[%v -> %v] Wire Send %x", c.identity.Hostname(), c.hostname, buffer) log.Debugf("[%v -> %v] Wire Send %x", c.identity.Hostname(), c.hostname, buffer)
_, err := c.conn.Write(buffer) _, err := c.conn.Write(buffer)
if err != nil { if err != nil {
c.closeInner() c.conn.Close()
c.closed = true
} }
return err
}
// Broadcast sends a message to all active service connections with a given capability
func (c *connection) Broadcast(message []byte, capability Capability) error {
return c.service.Broadcast(message, capability)
} }

View File

@ -9,7 +9,7 @@ go list ./... | xargs go vet
echo "" echo ""
echo "Linting:" echo "Linting:"
staticcheck ./... go list ./... | xargs golint
echo "Time to format" echo "Time to format"
@ -21,4 +21,4 @@ ineffassign .
# misspell (https://github.com/client9/misspell/cmd/misspell) # misspell (https://github.com/client9/misspell/cmd/misspell)
echo "Checking for misspelled words..." echo "Checking for misspelled words..."
misspell . | grep -v "testing/" | grep -v "vendor/" | grep -v "go.sum" | grep -v ".idea" misspell . | grep -v "vendor/" | grep -v "go.sum" | grep -v ".idea"

View File

@ -1,17 +1,15 @@
package testing package testing
import ( import (
"git.openprivacy.ca/cwtch.im/tapir" "cwtch.im/tapir"
"git.openprivacy.ca/cwtch.im/tapir/applications" "cwtch.im/tapir/applications"
"git.openprivacy.ca/cwtch.im/tapir/networks/tor" "cwtch.im/tapir/networks/tor"
"git.openprivacy.ca/cwtch.im/tapir/primitives" "cwtch.im/tapir/primitives"
"git.openprivacy.ca/openprivacy/connectivity" "git.openprivacy.ca/openprivacy/libricochet-go/connectivity"
torProvider "git.openprivacy.ca/openprivacy/connectivity/tor" "git.openprivacy.ca/openprivacy/libricochet-go/log"
"git.openprivacy.ca/openprivacy/log" "git.openprivacy.ca/openprivacy/libricochet-go/utils"
"golang.org/x/crypto/ed25519" "golang.org/x/crypto/ed25519"
"os"
"runtime" "runtime"
"runtime/pprof"
"sync" "sync"
"testing" "testing"
"time" "time"
@ -62,28 +60,16 @@ func TestTapir(t *testing.T) {
log.SetLevel(log.LevelDebug) log.SetLevel(log.LevelDebug)
log.Infof("Number of goroutines open at start: %d", runtime.NumGoroutine()) log.Infof("Number of goroutines open at start: %d", runtime.NumGoroutine())
// Connect to Tor // Connect to Tor
os.MkdirAll("./tor/", 0700) var acn connectivity.ACN
builder := new(torProvider.TorrcBuilder) acn, _ = connectivity.StartTor("./", "")
builder.WithSocksPort(9059).WithControlPort(9060).WithHashedPassword("tapir-integration-test").Build("./tor/torrc")
torDataDir := ""
var err error
if torDataDir, err = os.MkdirTemp("./tor/", "data-dir-"); err != nil {
t.Fatalf("could not create data dir")
}
// Connect to Tor
acn, err := torProvider.NewTorACNWithAuth("./", "", torDataDir, 9060, torProvider.HashedPasswordAuthenticator{Password: "tapir-integration-test"})
if err != nil {
t.Fatalf("could not launch ACN %v", err)
}
acn.WaitTillBootstrapped() acn.WaitTillBootstrapped()
// Generate Server Keys // Generate Server Keys
id, sk := primitives.InitializeEphemeralIdentity() id, sk := primitives.InitializeEphemeralIdentity()
// Init the Server running the Simple App. // Init the Server running the Simple App.
service := new(tor.BaseOnionService) var service tapir.Service
service = new(tor.BaseOnionService)
service.Init(acn, sk, &id) service.Init(acn, sk, &id)
// Goroutine Management // Goroutine Management
@ -100,59 +86,42 @@ func TestTapir(t *testing.T) {
wg.Add(2) wg.Add(2)
// Init a Client to Connect to the Server // Init a Client to Connect to the Server
client, clienthostname := genclient(acn) client, clienthostname := genclient(acn)
go connectclient(t, client, id.PublicKey(), wg) go connectclient(client, id.PublicKey(), wg)
CheckConnection(service, clienthostname, wg) CheckConnection(service, clienthostname, wg)
wg.Wait() wg.Wait()
// Wait for Garbage Collection... // Wait for Server to Sync
time.Sleep(time.Second * 60) time.Sleep(time.Second * 2)
log.Infof("Closing ACN...") log.Infof("Closing ACN...")
client.Shutdown()
service.Shutdown()
acn.Close() acn.Close()
sg.Wait() sg.Wait()
time.Sleep(time.Second * 5) time.Sleep(time.Second * 2)
log.Infof("Number of goroutines open at close: %d", runtime.NumGoroutine()) log.Infof("Number of goroutines open at close: %d", runtime.NumGoroutine())
if numRoutinesStart != runtime.NumGoroutine() { if numRoutinesStart != runtime.NumGoroutine() {
pprof.Lookup("goroutine").WriteTo(os.Stdout, 1)
t.Errorf("Potential goroutine leak: Num Start:%v NumEnd: %v", numRoutinesStart, runtime.NumGoroutine()) t.Errorf("Potential goroutine leak: Num Start:%v NumEnd: %v", numRoutinesStart, runtime.NumGoroutine())
} }
if !AuthSuccess { if !AuthSuccess {
t.Fatalf("Integration Test FAILED, client did not auth with server") t.Fatalf("Integration Test FAILED, client did not auth with server")
} }
} }
func genclient(acn connectivity.ACN) (tapir.Service, string) { func genclient(acn connectivity.ACN) (tapir.Service, string) {
id, sk := primitives.InitializeEphemeralIdentity() id, sk := primitives.InitializeEphemeralIdentity()
client := new(tor.BaseOnionService) var client tapir.Service
client = new(tor.BaseOnionService)
client.Init(acn, sk, &id) client.Init(acn, sk, &id)
return client, id.Hostname() return client, id.Hostname()
} }
// Client will Connect and launch it's own Echo App goroutine. // Client will Connect and launch it's own Echo App goroutine.
func connectclient(t *testing.T, client tapir.Service, key ed25519.PublicKey, group *sync.WaitGroup) { func connectclient(client tapir.Service, key ed25519.PublicKey, group *sync.WaitGroup) {
client.Connect(torProvider.GetTorV3Hostname(key), new(SimpleApp)) client.Connect(utils.GetTorV3Hostname(key), new(SimpleApp))
// Once connected, it shouldn't take long to authenticate and run the application. So for the purposes of this demo // Once connected, it shouldn't take long to authenticate and run the application. So for the purposes of this demo
// we will wait a little while then exit. // we will wait a little while then exit.
time.Sleep(time.Second * 5) time.Sleep(time.Second * 5)
conn, _ := client.GetConnection(torProvider.GetTorV3Hostname(key)) conn, _ := client.GetConnection(utils.GetTorV3Hostname(key))
log.Debugf("Client has Auth: %v", conn.HasCapability(applications.AuthCapability)) log.Debugf("Client has Auth: %v", conn.HasCapability(applications.AuthCapability))
if conn.HasCapability(applications.AuthCapability) == false {
t.Errorf("tapir auth failed")
}
// attempt to send a message that is too long
var long [8195]byte
err := conn.Send(long[:])
if err == nil {
t.Errorf("should have errored on message being too long...")
}
AuthSuccess = true AuthSuccess = true
group.Done() group.Done()
} }

View File

@ -1,14 +1,14 @@
package testing package testing
import ( import (
"git.openprivacy.ca/cwtch.im/tapir" "cwtch.im/tapir"
"git.openprivacy.ca/cwtch.im/tapir/applications" "cwtch.im/tapir/applications"
"git.openprivacy.ca/cwtch.im/tapir/networks/tor" "cwtch.im/tapir/networks/tor"
"git.openprivacy.ca/cwtch.im/tapir/primitives" "cwtch.im/tapir/primitives"
torProvider "git.openprivacy.ca/openprivacy/connectivity/tor" "git.openprivacy.ca/openprivacy/libricochet-go/connectivity"
"git.openprivacy.ca/openprivacy/log" "git.openprivacy.ca/openprivacy/libricochet-go/log"
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
"golang.org/x/crypto/ed25519" "golang.org/x/crypto/ed25519"
"os"
"runtime" "runtime"
"sync" "sync"
"testing" "testing"
@ -21,23 +21,8 @@ func TestTapirMaliciousRemote(t *testing.T) {
log.SetLevel(log.LevelDebug) log.SetLevel(log.LevelDebug)
log.Infof("Number of goroutines open at start: %d", runtime.NumGoroutine()) log.Infof("Number of goroutines open at start: %d", runtime.NumGoroutine())
// Connect to Tor // Connect to Tor
os.MkdirAll("./tor/", 0700) var acn connectivity.ACN
builder := new(torProvider.TorrcBuilder) acn, _ = connectivity.StartTor("./", "")
builder.WithHashedPassword("tapir-integration-test").Build("./tor/torrc")
// Connect to Tor
torDataDir := ""
var err error
if torDataDir, err = os.MkdirTemp("./tor/", "data-dir-"); err != nil {
t.Fatalf("could not create data dir")
}
// Connect to Tor
acn, err := torProvider.NewTorACNWithAuth("./", "", torDataDir, 9051, torProvider.HashedPasswordAuthenticator{Password: "tapir-integration-test"})
if err != nil {
t.Fatalf("could not launch ACN %v", err)
}
acn.WaitTillBootstrapped() acn.WaitTillBootstrapped()
// Generate Server Keys, not we generate two sets // Generate Server Keys, not we generate two sets
@ -45,7 +30,8 @@ func TestTapirMaliciousRemote(t *testing.T) {
id2, sk2 := primitives.InitializeEphemeralIdentity() id2, sk2 := primitives.InitializeEphemeralIdentity()
// Init the Server running the Simple App. // Init the Server running the Simple App.
service := new(tor.BaseOnionService) var service tapir.Service
service = new(tor.BaseOnionService)
// Initialize an onion service with one identity, but the auth app with another, this should // Initialize an onion service with one identity, but the auth app with another, this should
// trigger a failure in authentication protocol // trigger a failure in authentication protocol
service.Init(acn, sk2, &id) service.Init(acn, sk2, &id)
@ -70,11 +56,9 @@ func TestTapirMaliciousRemote(t *testing.T) {
// Wait for Server to Sync // Wait for Server to Sync
time.Sleep(time.Second * 2) time.Sleep(time.Second * 2)
log.Infof("closing ACN...") log.Infof("closing ACN...")
client.Shutdown()
service.Shutdown()
acn.Close() acn.Close()
sg.Wait() sg.Wait()
time.Sleep(time.Second * 5) // wait for goroutines to finish... time.Sleep(time.Second * 2)
log.Infof("Number of goroutines open at close: %d", runtime.NumGoroutine()) log.Infof("Number of goroutines open at close: %d", runtime.NumGoroutine())
if numRoutinesStart != runtime.NumGoroutine() { if numRoutinesStart != runtime.NumGoroutine() {
t.Errorf("Potential goroutine leak: Num Start:%v NumEnd: %v", numRoutinesStart, runtime.NumGoroutine()) t.Errorf("Potential goroutine leak: Num Start:%v NumEnd: %v", numRoutinesStart, runtime.NumGoroutine())
@ -83,17 +67,17 @@ func TestTapirMaliciousRemote(t *testing.T) {
// Client will Connect and launch it's own Echo App goroutine. // Client will Connect and launch it's own Echo App goroutine.
func connectclientandfail(client tapir.Service, key ed25519.PublicKey, group *sync.WaitGroup, t *testing.T) { func connectclientandfail(client tapir.Service, key ed25519.PublicKey, group *sync.WaitGroup, t *testing.T) {
client.Connect(torProvider.GetTorV3Hostname(key), new(applications.AuthApp)) client.Connect(utils.GetTorV3Hostname(key), new(applications.AuthApp))
// Once connected, it shouldn't take long to authenticate and run the application. So for the purposes of this demo // Once connected, it shouldn't take long to authenticate and run the application. So for the purposes of this demo
// we will wait a little while then exit. // we will wait a little while then exit.
time.Sleep(time.Second * 5) time.Sleep(time.Second * 5)
log.Infof("Checking connection status...") log.Infof("Checking connection status...")
conn, err := client.GetConnection(torProvider.GetTorV3Hostname(key)) conn, err := client.GetConnection(utils.GetTorV3Hostname(key))
if err == nil { if err == nil {
group.Done() group.Done()
t.Errorf("Connection should have failed! %v %v", conn, err) t.Fatalf("Connection should have failed! %v %v", conn, err)
} }
log.Infof("Successfully failed to authenticate...") log.Infof("Successfully failed to authenticate...")
group.Done() group.Done()

View File

@ -4,18 +4,11 @@ set -e
pwd pwd
go test -race ${1} -coverprofile=applications.cover.out -v ./applications go test -race ${1} -coverprofile=applications.cover.out -v ./applications
go test -race ${1} -coverprofile=applications.tokenboard.cover.out -v ./applications/tokenboard go test -race ${1} -coverprofile=applications.tokenboard.cover.out -v ./applications/tokenboard
# persistence is broken in WSL
if grep -q -v Microsoft /proc/version; then
go test -race ${1} -coverprofile=persistence.cover.out -v ./persistence
fi
go test -race ${1} -coverprofile=primitives.cover.out -v ./primitives go test -race ${1} -coverprofile=primitives.cover.out -v ./primitives
go test -race ${1} -coverprofile=primitives.auditable.cover.out -v ./primitives/auditable go test -race ${1} -coverprofile=primitives.auditable.cover.out -v ./primitives/auditable
go test -race ${1} -coverprofile=primitives.core.cover.out -v ./primitives/core go test -race ${1} -coverprofile=primitives.core.cover.out -v ./primitives/core
# persistence is broken in WSL go test -race ${1} -coverprofile=primitives.privacypass.cover.out -v ./primitives/privacypass
if grep -q -v Microsoft /proc/version; then go test -bench "BenchmarkAuditableStore" -benchtime 1000x primitives/auditable/*.go
go test -race ${1} -coverprofile=primitives.privacypass.cover.out -v ./primitives/privacypass
go test -bench "BenchmarkAuditableStore" -benchtime 1000x primitives/auditable/*.go
fi
echo "mode: set" > coverage.out && cat *.cover.out | grep -v mode: | sort -r | \ echo "mode: set" > coverage.out && cat *.cover.out | grep -v mode: | sort -r | \
awk '{if($1 != last) {print $0;last=$1}}' >> coverage.out awk '{if($1 != last) {print $0;last=$1}}' >> coverage.out
rm -rf *.cover.out rm -rf *.cover.out

View File

@ -1,50 +0,0 @@
package utils
import (
"crypto/sha512"
"filippo.io/edwards25519"
"golang.org/x/crypto/curve25519"
"golang.org/x/crypto/ed25519"
)
// EDH implements diffie hellman using curve25519 keys derived from ed25519 keys
func EDH(privateKey ed25519.PrivateKey, remotePublicKey ed25519.PublicKey) ([]byte, error) {
var privKeyBytes [64]byte
var remotePubKeyBytes [32]byte
copy(privKeyBytes[:], privateKey[:])
copy(remotePubKeyBytes[:], remotePublicKey[:])
var curve25519priv [32]byte
PrivateKeyToCurve25519(&curve25519priv, &privKeyBytes)
remoteCurve25519pub, err := ed25519PublicKeyToCurve25519New(remotePublicKey)
if err != nil {
return []byte{}, err
}
secret, err := curve25519.X25519(curve25519priv[:], remoteCurve25519pub[:])
return secret, err
}
// reproduced from https://github.com/FiloSottile/age/blob/main/agessh/agessh.go#L190
func ed25519PublicKeyToCurve25519New(pk ed25519.PublicKey) ([]byte, error) {
// See https://blog.filippo.io/using-ed25519-keys-for-encryption and
// https://pkg.go.dev/filippo.io/edwards25519#Point.BytesMontgomery.
p, err := new(edwards25519.Point).SetBytes(pk)
if err != nil {
return nil, err
}
return p.BytesMontgomery(), nil
}
// PrivateKeyToCurve25519 converts an ed25519 private key into a corresponding
// curve25519 private key
func PrivateKeyToCurve25519(curve25519Private *[32]byte, privateKey *[64]byte) {
h := sha512.New()
h.Write(privateKey[:32])
digest := h.Sum(nil)
digest[0] &= 248
digest[31] &= 127
digest[31] |= 64
copy(curve25519Private[:], digest)
}