forked from cwtch.im/tapir
First cut of Token Board
This commit is contained in:
parent
5b64c2d708
commit
345d11f506
|
@ -3,3 +3,4 @@ vendor/
|
||||||
/tor/
|
/tor/
|
||||||
coverage.out
|
coverage.out
|
||||||
/testing/tor/
|
/testing/tor/
|
||||||
|
/applications/tor/
|
||||||
|
|
|
@ -0,0 +1,192 @@
|
||||||
|
package applications
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cwtch.im/tapir"
|
||||||
|
"cwtch.im/tapir/primitives"
|
||||||
|
"cwtch.im/tapir/primitives/privacypass"
|
||||||
|
"encoding/json"
|
||||||
|
"git.openprivacy.ca/openprivacy/libricochet-go/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TokenBoardApp defines a Tapir Meta=App which provides a global cryptographic transcript
|
||||||
|
type TokenBoardApp struct {
|
||||||
|
AuthApp
|
||||||
|
connection tapir.Connection
|
||||||
|
TokenService *privacypass.TokenServer
|
||||||
|
AuditableStore *primitives.AuditableStore
|
||||||
|
paymentHandler privacypass.TokenPaymentHandler
|
||||||
|
handler TokenBoardAppHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenBoardAppHandler allows clients to react to specific events.
|
||||||
|
type TokenBoardAppHandler interface {
|
||||||
|
HandleNewMessages(previousLastCommit []byte)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTokenBoardClient generates a new Client for Token Board
|
||||||
|
func NewTokenBoardClient(store *primitives.AuditableStore, handler TokenBoardAppHandler, paymentHandler privacypass.TokenPaymentHandler) tapir.Application {
|
||||||
|
tba := new(TokenBoardApp)
|
||||||
|
tba.TokenService = nil
|
||||||
|
tba.AuditableStore = store
|
||||||
|
tba.handler = handler
|
||||||
|
tba.paymentHandler = paymentHandler
|
||||||
|
return tba
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTokenBoardServer generates new Server for Token Board
|
||||||
|
func NewTokenBoardServer(tokenService *privacypass.TokenServer, store *primitives.AuditableStore) tapir.Application {
|
||||||
|
tba := new(TokenBoardApp)
|
||||||
|
tba.TokenService = tokenService
|
||||||
|
tba.AuditableStore = store
|
||||||
|
return tba
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInstance creates a new TokenBoardApp
|
||||||
|
func (ta *TokenBoardApp) NewInstance() tapir.Application {
|
||||||
|
tba := new(TokenBoardApp)
|
||||||
|
tba.TokenService = ta.TokenService
|
||||||
|
tba.AuditableStore = ta.AuditableStore
|
||||||
|
tba.handler = ta.handler
|
||||||
|
tba.paymentHandler = ta.paymentHandler
|
||||||
|
return tba
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init initializes the cryptographic TokenBoardApp
|
||||||
|
func (ta *TokenBoardApp) Init(connection tapir.Connection) {
|
||||||
|
ta.AuthApp.Init(connection)
|
||||||
|
|
||||||
|
if connection.HasCapability(AuthCapability) {
|
||||||
|
ta.connection = connection
|
||||||
|
// If we are a server, now we can start listening for inbound messages
|
||||||
|
if ta.connection.IsOutbound() {
|
||||||
|
go ta.listen(ta.clientSwitch)
|
||||||
|
} else {
|
||||||
|
go ta.listen(ta.serverSwitch)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
connection.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenBoardMessage encapsulates the application protocol
|
||||||
|
type TokenBoardMessage struct {
|
||||||
|
MessageType string
|
||||||
|
PostRequest PostRequest `json:",omitempty"`
|
||||||
|
PostResult PostResult `json:",omitempty"`
|
||||||
|
ReplayRequest ReplayRequest `json:",omitempty"`
|
||||||
|
ReplayResponse ReplayResponse `json:",omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplayRequest requests a reply from the given Commit
|
||||||
|
type ReplayRequest struct {
|
||||||
|
LastCommit []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostRequest requests to post the message to the board with the given token
|
||||||
|
type PostRequest struct {
|
||||||
|
Token privacypass.SpentToken
|
||||||
|
Message primitives.Message
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostResult returns the success of a given post attempt
|
||||||
|
type PostResult struct {
|
||||||
|
Success bool
|
||||||
|
Proof primitives.SignedProof
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplayResponse is sent by the server before a stream of replayed messages
|
||||||
|
type ReplayResponse struct {
|
||||||
|
NumMessages int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ta *TokenBoardApp) clientSwitch(message TokenBoardMessage) {
|
||||||
|
switch message.MessageType {
|
||||||
|
case "PostResult":
|
||||||
|
log.Debugf("Post result: %x", message.PostResult.Proof)
|
||||||
|
case "ReplayResponse":
|
||||||
|
var state primitives.State
|
||||||
|
log.Debugf("Replaying %v Messages...", message.ReplayResponse.NumMessages)
|
||||||
|
lastCommit := ta.AuditableStore.LatestCommit
|
||||||
|
for i := 0; i < message.ReplayResponse.NumMessages; i++ {
|
||||||
|
message := ta.connection.Expect()
|
||||||
|
state.Messages = append(state.Messages, message)
|
||||||
|
}
|
||||||
|
data := ta.connection.Expect()
|
||||||
|
var signedProof primitives.SignedProof
|
||||||
|
json.Unmarshal(data, &signedProof)
|
||||||
|
err := ta.AuditableStore.MergeState(state, signedProof)
|
||||||
|
if err == nil {
|
||||||
|
log.Debugf("Successfully updated Auditable Store")
|
||||||
|
ta.handler.HandleNewMessages(lastCommit)
|
||||||
|
} else {
|
||||||
|
log.Debugf("Error updating Auditable Store %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ta *TokenBoardApp) serverSwitch(message TokenBoardMessage) {
|
||||||
|
switch message.MessageType {
|
||||||
|
case "PostRequest":
|
||||||
|
postrequest := message.PostRequest
|
||||||
|
log.Debugf("Received a Post Message Request: %x %x", postrequest.Token, postrequest.Message)
|
||||||
|
ta.postMessageRequest(postrequest.Token, postrequest.Message)
|
||||||
|
case "ReplayRequest":
|
||||||
|
state, proof := ta.AuditableStore.GetState()
|
||||||
|
response, _ := json.Marshal(TokenBoardMessage{MessageType: "ReplayResponse", ReplayResponse: ReplayResponse{len(state.Messages)}})
|
||||||
|
ta.connection.Send(response)
|
||||||
|
for _, message := range state.Messages {
|
||||||
|
ta.connection.Send(message)
|
||||||
|
}
|
||||||
|
data, _ := json.Marshal(proof)
|
||||||
|
ta.connection.Send(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ta *TokenBoardApp) listen(switchFn func(TokenBoardMessage)) {
|
||||||
|
for {
|
||||||
|
data := ta.connection.Expect()
|
||||||
|
if len(data) == 0 {
|
||||||
|
return // connection is closed
|
||||||
|
}
|
||||||
|
|
||||||
|
var message TokenBoardMessage
|
||||||
|
json.Unmarshal(data, &message)
|
||||||
|
log.Debugf("Received a Message: %v", message)
|
||||||
|
switchFn(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replay posts a Replay Message to the server.
|
||||||
|
func (ta *TokenBoardApp) Replay() {
|
||||||
|
data, _ := json.Marshal(TokenBoardMessage{MessageType: "ReplayRequest"})
|
||||||
|
ta.connection.Send(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PurchaseTokens purchases the given number of tokens from the server (using the provided payment handler)
|
||||||
|
func (ta *TokenBoardApp) PurchaseTokens(num int) {
|
||||||
|
ta.paymentHandler.MakePayment(num)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post sends a Post Request to the server
|
||||||
|
func (ta *TokenBoardApp) Post(message primitives.Message) bool {
|
||||||
|
token, err := ta.paymentHandler.NextToken(message)
|
||||||
|
if err == nil {
|
||||||
|
data, _ := json.Marshal(TokenBoardMessage{MessageType: "PostRequest", PostRequest: PostRequest{Token: token, Message: message}})
|
||||||
|
ta.connection.Send(data)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ta *TokenBoardApp) postMessageRequest(token privacypass.SpentToken, message primitives.Message) {
|
||||||
|
if ta.TokenService.IsValid(token, message) {
|
||||||
|
log.Debugf("Token is valid")
|
||||||
|
signedproof := ta.AuditableStore.Add(message)
|
||||||
|
data, _ := json.Marshal(TokenBoardMessage{MessageType: "PostResult", PostResult: PostResult{true, signedproof}})
|
||||||
|
ta.connection.Send(data)
|
||||||
|
} else {
|
||||||
|
log.Debugf("Attempt to spend an invalid token")
|
||||||
|
data, _ := json.Marshal(TokenBoardMessage{MessageType: "PostResult", PostResult: PostResult{false, primitives.SignedProof{}}})
|
||||||
|
ta.connection.Send(data)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,118 @@
|
||||||
|
package applications
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cwtch.im/tapir"
|
||||||
|
"cwtch.im/tapir/networks/tor"
|
||||||
|
"cwtch.im/tapir/primitives"
|
||||||
|
"cwtch.im/tapir/primitives/core"
|
||||||
|
"cwtch.im/tapir/primitives/privacypass"
|
||||||
|
"errors"
|
||||||
|
"git.openprivacy.ca/openprivacy/libricochet-go/connectivity"
|
||||||
|
"git.openprivacy.ca/openprivacy/libricochet-go/log"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Handler struct {
|
||||||
|
Store *primitives.AuditableStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h Handler) HandleNewMessages(previousLastCommit []byte) {
|
||||||
|
log.Debugf("Handling Messages After %x", previousLastCommit)
|
||||||
|
messages := h.Store.GetMessagesAfter(previousLastCommit)
|
||||||
|
for _, message := range messages {
|
||||||
|
log.Debugf("Message %s", message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type FreePaymentHandler struct {
|
||||||
|
tokens []*privacypass.Token
|
||||||
|
TokenService *privacypass.TokenServer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fph *FreePaymentHandler) MakePayment(int) {
|
||||||
|
tokens, blindedTokens := privacypass.GenerateBlindedTokenBatch(10)
|
||||||
|
// Obtained some signed tokens, in reality these would be bought and paid for through some other mechanism.
|
||||||
|
clientTranscript := core.NewTranscript("privacyPass")
|
||||||
|
serverTranscript := core.NewTranscript("privacyPass")
|
||||||
|
|
||||||
|
signedTokens, proof := fph.TokenService.SignBlindedTokenBatch(blindedTokens, serverTranscript)
|
||||||
|
privacypass.UnblindSignedTokenBatch(tokens, blindedTokens, signedTokens, fph.TokenService.Y, proof, clientTranscript)
|
||||||
|
fph.tokens = append(fph.tokens, tokens...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fph *FreePaymentHandler) NextToken(data []byte) (privacypass.SpentToken, error) {
|
||||||
|
if len(fph.tokens) == 0 {
|
||||||
|
return privacypass.SpentToken{}, errors.New("No more tokens")
|
||||||
|
}
|
||||||
|
token := fph.tokens[0]
|
||||||
|
fph.tokens = fph.tokens[1:]
|
||||||
|
return token.SpendToken(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTokenBoardApp(t *testing.T) {
|
||||||
|
// numRoutinesStart := runtime.NumGoroutine()
|
||||||
|
log.SetLevel(log.LevelDebug)
|
||||||
|
log.Infof("Number of goroutines open at start: %d", runtime.NumGoroutine())
|
||||||
|
// Connect to Tor
|
||||||
|
var acn connectivity.ACN
|
||||||
|
acn, _ = connectivity.StartTor("./", "")
|
||||||
|
acn.WaitTillBootstrapped()
|
||||||
|
|
||||||
|
// Generate Server Key
|
||||||
|
sid, sk := primitives.InitializeEphemeralIdentity()
|
||||||
|
|
||||||
|
tokenService := privacypass.NewTokenServer()
|
||||||
|
serverAuditableStore := new(primitives.AuditableStore)
|
||||||
|
serverAuditableStore.Init(sid)
|
||||||
|
|
||||||
|
clientAuditableStore := new(primitives.AuditableStore)
|
||||||
|
clientAuditableStore.Init(sid)
|
||||||
|
|
||||||
|
// Init the Server running the Simple App.
|
||||||
|
var service tapir.Service
|
||||||
|
service = new(tor.BaseOnionService)
|
||||||
|
service.Init(acn, sk, &sid)
|
||||||
|
|
||||||
|
// Goroutine Management
|
||||||
|
sg := new(sync.WaitGroup)
|
||||||
|
sg.Add(1)
|
||||||
|
go func() {
|
||||||
|
service.Listen(NewTokenBoardServer(&tokenService, serverAuditableStore))
|
||||||
|
sg.Done()
|
||||||
|
}()
|
||||||
|
|
||||||
|
time.Sleep(time.Second * 30)
|
||||||
|
id, sk := primitives.InitializeEphemeralIdentity()
|
||||||
|
var client tapir.Service
|
||||||
|
client = new(tor.BaseOnionService)
|
||||||
|
client.Init(acn, sk, &id)
|
||||||
|
client.Connect(sid.Hostname(), NewTokenBoardClient(clientAuditableStore, Handler{Store: clientAuditableStore}, &FreePaymentHandler{TokenService: &tokenService}))
|
||||||
|
client.WaitForCapabilityOrClose(sid.Hostname(), AuthCapability)
|
||||||
|
conn, _ := client.GetConnection(sid.Hostname())
|
||||||
|
tba, _ := conn.App().(*TokenBoardApp)
|
||||||
|
tba.PurchaseTokens(10)
|
||||||
|
tba.Post([]byte("HELLO 1"))
|
||||||
|
tba.Post([]byte("HELLO 2"))
|
||||||
|
tba.Post([]byte("HELLO 3"))
|
||||||
|
tba.Post([]byte("HELLO 4"))
|
||||||
|
tba.Post([]byte("HELLO 5"))
|
||||||
|
|
||||||
|
tba.Replay()
|
||||||
|
tba.Post([]byte("HELLO 6"))
|
||||||
|
tba.Post([]byte("HELLO 7"))
|
||||||
|
tba.Post([]byte("HELLO 8"))
|
||||||
|
tba.Post([]byte("HELLO 9"))
|
||||||
|
tba.Post([]byte("HELLO 10"))
|
||||||
|
|
||||||
|
tba.Replay()
|
||||||
|
|
||||||
|
if tba.Post([]byte("HELLO 11")) {
|
||||||
|
t.Errorf("Post should have failed.")
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(time.Second * 60)
|
||||||
|
|
||||||
|
}
|
|
@ -1,22 +1,25 @@
|
||||||
package primitives
|
package primitives
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/subtle"
|
|
||||||
"cwtch.im/tapir/primitives/core"
|
"cwtch.im/tapir/primitives/core"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"golang.org/x/crypto/ed25519"
|
"golang.org/x/crypto/ed25519"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SignedProof encapsulates a signed proof
|
// SignedProof encapsulates a signed proof
|
||||||
type SignedProof []byte
|
type SignedProof struct {
|
||||||
|
Commit []byte
|
||||||
|
Proof []byte
|
||||||
|
}
|
||||||
|
|
||||||
// Message encapsulates a message for more readable code.
|
// Message encapsulates a message for more readable code.
|
||||||
type Message []byte
|
type Message []byte
|
||||||
|
|
||||||
// State defines an array of messages.
|
// State defines an array of messages.
|
||||||
type State struct {
|
type State struct {
|
||||||
message []Message
|
Messages []Message
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuditableStore defines a cryptographically secure & auditable transcript of messages sent from multiple
|
// AuditableStore defines a cryptographically secure & auditable transcript of messages sent from multiple
|
||||||
|
@ -25,50 +28,64 @@ type AuditableStore struct {
|
||||||
state State
|
state State
|
||||||
identity Identity
|
identity Identity
|
||||||
transcript *core.Transcript
|
transcript *core.Transcript
|
||||||
latestCommit []byte
|
LatestCommit []byte
|
||||||
commits map[string]bool
|
commits map[string]int
|
||||||
|
mutex sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init initializes an auditable store
|
// Init initializes an auditable store
|
||||||
func (as *AuditableStore) Init(identity Identity) {
|
func (as *AuditableStore) Init(identity Identity) {
|
||||||
as.identity = identity
|
as.identity = identity
|
||||||
as.transcript = core.NewTranscript("auditable-data-store")
|
as.transcript = core.NewTranscript("auditable-data-store")
|
||||||
as.commits = make(map[string]bool)
|
as.commits = make(map[string]int)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add adds a message to the auditable store
|
// Add adds a message to the auditable store
|
||||||
func (as *AuditableStore) Add(message Message, latestCommit []byte) ([]byte, SignedProof, error) {
|
func (as *AuditableStore) Add(message Message) SignedProof {
|
||||||
if subtle.ConstantTimeCompare(latestCommit, as.latestCommit) == 1 {
|
as.mutex.Lock()
|
||||||
as.state.message = append(as.state.message, message)
|
defer as.mutex.Unlock()
|
||||||
as.transcript.AddToTranscript("new-message", message)
|
as.state.Messages = append(as.state.Messages, message)
|
||||||
as.latestCommit = as.identity.Sign(as.transcript.CommitToTranscript("commit"))
|
as.transcript.AddToTranscript("new-message", message)
|
||||||
return as.latestCommit, as.identity.Sign(as.latestCommit), nil
|
as.LatestCommit = as.identity.Sign(as.transcript.CommitToTranscript("commit"))
|
||||||
}
|
return SignedProof{as.LatestCommit, as.identity.Sign(as.LatestCommit)}
|
||||||
// this prevents multiple clients updating at the same time and will likely cause retry storms.
|
|
||||||
return nil, nil, errors.New("attempt to append out of date transcript")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetState returns the current auditable state
|
// GetState returns the current auditable state
|
||||||
func (as *AuditableStore) GetState() (State, []byte, SignedProof) {
|
func (as *AuditableStore) GetState() (State, SignedProof) {
|
||||||
return as.state, as.latestCommit, as.identity.Sign(as.latestCommit)
|
as.mutex.Lock()
|
||||||
|
defer as.mutex.Unlock()
|
||||||
|
return as.state, SignedProof{as.LatestCommit, as.identity.Sign(as.LatestCommit)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMessagesAfter provides access to messages after the given commit.
|
||||||
|
func (as *AuditableStore) GetMessagesAfter(latestCommit []byte) []Message {
|
||||||
|
as.mutex.Lock()
|
||||||
|
defer as.mutex.Unlock()
|
||||||
|
index, ok := as.commits[base64.StdEncoding.EncodeToString(latestCommit)]
|
||||||
|
if !ok && len(latestCommit) == 32 {
|
||||||
|
return []Message{}
|
||||||
|
} else if len(latestCommit) == 0 {
|
||||||
|
index = -1
|
||||||
|
}
|
||||||
|
return as.state.Messages[index+1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
// MergeState merges a given state onto our state, first verifying that the two transcripts align
|
// MergeState merges a given state onto our state, first verifying that the two transcripts align
|
||||||
func (as *AuditableStore) MergeState(state State, signedStateProof SignedProof, key ed25519.PublicKey) error {
|
func (as *AuditableStore) MergeState(state State, signedStateProof SignedProof) error {
|
||||||
next := len(as.state.message)
|
next := len(as.state.Messages)
|
||||||
for _, m := range state.message[next:] {
|
for i, m := range state.Messages[next:] {
|
||||||
as.state.message = append(as.state.message, m)
|
as.state.Messages = append(as.state.Messages, m)
|
||||||
|
|
||||||
// We reconstruct the transcript
|
// We reconstruct the transcript
|
||||||
as.transcript.AddToTranscript("new-message", m)
|
as.transcript.AddToTranscript("new-message", m)
|
||||||
as.latestCommit = as.identity.Sign(as.transcript.CommitToTranscript("commit"))
|
as.LatestCommit = as.identity.Sign(as.transcript.CommitToTranscript("commit"))
|
||||||
as.commits[base64.StdEncoding.EncodeToString(as.latestCommit)] = true
|
as.commits[base64.StdEncoding.EncodeToString(as.LatestCommit)] = next + i
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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(key, as.latestCommit, signedStateProof) == false {
|
if ed25519.Verify(as.identity.PublicKey(), as.LatestCommit, signedStateProof.Proof) == 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
|
||||||
|
@ -84,16 +101,16 @@ func (as *AuditableStore) MergeState(state State, signedStateProof SignedProof,
|
||||||
// If, after syncing, the FraudProof still validates, then the server must be malicious.
|
// If, after syncing, the FraudProof still validates, then the server must be malicious.
|
||||||
// the information revealed by publicizing a fraud proof is minimal it only reveals the inconsistent transcript commit
|
// the information revealed by publicizing a fraud proof is minimal it only reveals the inconsistent transcript commit
|
||||||
// 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 *AuditableStore) VerifyFraudProof(commit []byte, signedFraudProof SignedProof, key ed25519.PublicKey) (bool, error) {
|
func (as *AuditableStore) VerifyFraudProof(signedFraudProof SignedProof, key ed25519.PublicKey) (bool, error) {
|
||||||
|
|
||||||
if ed25519.Verify(key, commit, signedFraudProof) == false {
|
if ed25519.Verify(key, signedFraudProof.Commit, signedFraudProof.Proof) == 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
|
||||||
return false, errors.New("signed proof has not been signed by the given public key")
|
return false, errors.New("signed proof has not been signed by the given public key")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, exists := as.commits[base64.StdEncoding.EncodeToString(commit)]
|
_, exists := as.commits[base64.StdEncoding.EncodeToString(signedFraudProof.Commit)]
|
||||||
if !exists {
|
if !exists {
|
||||||
// We have a message signed by the server which verifies that a message was inserted into the state at a given index
|
// We have a message signed by the server which verifies that a message was inserted into the state at a given index
|
||||||
// However this directly contradicts our version of the state.
|
// However this directly contradicts our version of the state.
|
||||||
|
|
|
@ -12,22 +12,22 @@ func TestAuditableStore(t *testing.T) {
|
||||||
as.Init(serverID)
|
as.Init(serverID)
|
||||||
vs.Init(serverID) // This doesn't do anything
|
vs.Init(serverID) // This doesn't do anything
|
||||||
|
|
||||||
as.Add([]byte("Hello World"), as.latestCommit)
|
as.Add([]byte("Hello World"))
|
||||||
state, _, proof := as.GetState()
|
state, proof := as.GetState()
|
||||||
|
|
||||||
if vs.MergeState(state, proof, serverID.PublicKey()) != nil {
|
if vs.MergeState(state, proof) != nil {
|
||||||
t.Fatalf("Fraud Proof Failed on Honest Proof")
|
t.Fatalf("Fraud Proof Failed on Honest Proof")
|
||||||
}
|
}
|
||||||
|
|
||||||
commit, fraudProof, _ := as.Add([]byte("Hello World 2"), as.latestCommit)
|
fraudProof := as.Add([]byte("Hello World 2"))
|
||||||
|
|
||||||
// If you comment these out it simulates a lying server.
|
// If you comment these out it simulates a lying server.
|
||||||
state, _, proof = as.GetState()
|
state, proof = as.GetState()
|
||||||
if vs.MergeState(state, proof, serverID.PublicKey()) != nil {
|
if vs.MergeState(state, proof) != nil {
|
||||||
t.Fatalf("Fraud Proof Failed on Honest Proof")
|
t.Fatalf("Fraud Proof Failed on Honest Proof")
|
||||||
}
|
}
|
||||||
|
|
||||||
fraud, err := vs.VerifyFraudProof(commit, fraudProof, serverID.PublicKey())
|
fraud, err := vs.VerifyFraudProof(fraudProof, serverID.PublicKey())
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Error validated fraud proof: %v", err)
|
t.Fatalf("Error validated fraud proof: %v", err)
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"cwtch.im/tapir/primitives/core"
|
"cwtch.im/tapir/primitives/core"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git.openprivacy.ca/openprivacy/libricochet-go/log"
|
||||||
"github.com/bwesterb/go-ristretto"
|
"github.com/bwesterb/go-ristretto"
|
||||||
"golang.org/x/crypto/sha3"
|
"golang.org/x/crypto/sha3"
|
||||||
)
|
)
|
||||||
|
@ -29,10 +30,16 @@ type SignedToken struct {
|
||||||
|
|
||||||
// SpentToken encapsulates the parameters needed to spend a Token
|
// SpentToken encapsulates the parameters needed to spend a Token
|
||||||
type SpentToken struct {
|
type SpentToken struct {
|
||||||
t []byte
|
T []byte
|
||||||
MAC []byte
|
MAC []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TokenPaymentHandler defines an interface with external payment processors
|
||||||
|
type TokenPaymentHandler interface {
|
||||||
|
MakePayment(int)
|
||||||
|
NextToken(data []byte) (SpentToken, error)
|
||||||
|
}
|
||||||
|
|
||||||
// GenBlindedToken initializes the Token
|
// GenBlindedToken initializes the Token
|
||||||
// GenToken() & Blind()
|
// GenToken() & Blind()
|
||||||
func (t *Token) GenBlindedToken() BlindedToken {
|
func (t *Token) GenBlindedToken() BlindedToken {
|
||||||
|
@ -41,6 +48,7 @@ func (t *Token) GenBlindedToken() BlindedToken {
|
||||||
t.r = new(ristretto.Scalar).Rand()
|
t.r = new(ristretto.Scalar).Rand()
|
||||||
|
|
||||||
Ht := sha3.Sum256(t.t)
|
Ht := sha3.Sum256(t.t)
|
||||||
|
log.Debugf("token: %x", Ht)
|
||||||
T := new(ristretto.Point).SetElligator(&Ht)
|
T := new(ristretto.Point).SetElligator(&Ht)
|
||||||
P := new(ristretto.Point).ScalarMult(T, t.r)
|
P := new(ristretto.Point).ScalarMult(T, t.r)
|
||||||
return BlindedToken{P}
|
return BlindedToken{P}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package privacypass
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"cwtch.im/tapir/primitives/core"
|
"cwtch.im/tapir/primitives/core"
|
||||||
|
"git.openprivacy.ca/openprivacy/libricochet-go/log"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -30,6 +31,7 @@ func TestToken_SpendToken(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGenerateBlindedTokenBatch(t *testing.T) {
|
func TestGenerateBlindedTokenBatch(t *testing.T) {
|
||||||
|
log.SetLevel(log.LevelDebug)
|
||||||
server := NewTokenServer()
|
server := NewTokenServer()
|
||||||
|
|
||||||
clientTranscript := core.NewTranscript("privacyPass")
|
clientTranscript := core.NewTranscript("privacyPass")
|
||||||
|
|
|
@ -5,21 +5,24 @@ import (
|
||||||
"cwtch.im/tapir/primitives/core"
|
"cwtch.im/tapir/primitives/core"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git.openprivacy.ca/openprivacy/libricochet-go/log"
|
||||||
"github.com/bwesterb/go-ristretto"
|
"github.com/bwesterb/go-ristretto"
|
||||||
"golang.org/x/crypto/sha3"
|
"golang.org/x/crypto/sha3"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TokenServer implements a token server.
|
// TokenServer implements a token server.
|
||||||
type TokenServer struct {
|
type TokenServer struct {
|
||||||
k *ristretto.Scalar
|
k *ristretto.Scalar
|
||||||
Y *ristretto.Point
|
Y *ristretto.Point
|
||||||
seen map[string]bool
|
seen map[string]bool
|
||||||
|
mutex sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 {
|
||||||
k := new(ristretto.Scalar).Rand()
|
k := new(ristretto.Scalar).Rand()
|
||||||
return TokenServer{k, new(ristretto.Point).ScalarMultBase(k), make(map[string]bool)}
|
return TokenServer{k, new(ristretto.Point).ScalarMultBase(k), make(map[string]bool), sync.Mutex{}}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SignBlindedToken calculates kP for the given BlindedToken P
|
// SignBlindedToken calculates kP for the given BlindedToken P
|
||||||
|
@ -57,18 +60,23 @@ func (ts *TokenServer) constructBatchProof(blindedTokens []BlindedToken, signedT
|
||||||
|
|
||||||
// IsValid returns true a SpentToken is valid and has never been spent before, false otherwise.
|
// IsValid returns true a SpentToken is valid and has never been spent before, false otherwise.
|
||||||
func (ts *TokenServer) IsValid(token SpentToken, data []byte) bool {
|
func (ts *TokenServer) IsValid(token SpentToken, data []byte) bool {
|
||||||
if _, spent := ts.seen[hex.EncodeToString(token.t)]; spent {
|
log.Debugf("data: [%s]", data)
|
||||||
|
ts.mutex.Lock()
|
||||||
|
defer ts.mutex.Unlock() // We only want 1 client at a time redeeming tokens to prevent double-spends
|
||||||
|
if _, spent := ts.seen[hex.EncodeToString(token.T)]; spent {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
Ht := sha3.Sum256(token.t)
|
Ht := sha3.Sum256(token.T)
|
||||||
|
log.Debugf("token: %x", Ht)
|
||||||
T := new(ristretto.Point).SetElligator(&Ht)
|
T := new(ristretto.Point).SetElligator(&Ht)
|
||||||
W := new(ristretto.Point).ScalarMult(T, ts.k)
|
W := new(ristretto.Point).ScalarMult(T, ts.k)
|
||||||
key := sha3.Sum256(append(token.t, W.Bytes()...))
|
key := sha3.Sum256(append(token.T, W.Bytes()...))
|
||||||
mac := hmac.New(sha3.New512, key[:])
|
mac := hmac.New(sha3.New512, key[:])
|
||||||
K := mac.Sum(data)
|
K := mac.Sum(data)
|
||||||
|
log.Debugf("mac: \n%x\nK:%x\n", token.MAC, K)
|
||||||
result := hmac.Equal(token.MAC, K)
|
result := hmac.Equal(token.MAC, K)
|
||||||
if result == true {
|
if result == true {
|
||||||
ts.seen[hex.EncodeToString(token.t)] = true
|
ts.seen[hex.EncodeToString(token.T)] = true
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue