diff --git a/.gitignore b/.gitignore index 4ede555..003fb79 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ vendor/ /tor/ coverage.out /testing/tor/ +/applications/tor/ diff --git a/applications/tokenboard.go b/applications/tokenboard.go new file mode 100644 index 0000000..d471e8a --- /dev/null +++ b/applications/tokenboard.go @@ -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) + } +} diff --git a/applications/tokenboard_integration_test.go b/applications/tokenboard_integration_test.go new file mode 100644 index 0000000..285585d --- /dev/null +++ b/applications/tokenboard_integration_test.go @@ -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) + +} diff --git a/primitives/auditablestore.go b/primitives/auditablestore.go index df839f6..bd33593 100644 --- a/primitives/auditablestore.go +++ b/primitives/auditablestore.go @@ -1,22 +1,25 @@ package primitives import ( - "crypto/subtle" "cwtch.im/tapir/primitives/core" "encoding/base64" "errors" "golang.org/x/crypto/ed25519" + "sync" ) // SignedProof encapsulates a signed proof -type SignedProof []byte +type SignedProof struct { + Commit []byte + Proof []byte +} // Message encapsulates a message for more readable code. type Message []byte // State defines an array of messages. type State struct { - message []Message + Messages []Message } // AuditableStore defines a cryptographically secure & auditable transcript of messages sent from multiple @@ -25,50 +28,64 @@ type AuditableStore struct { state State identity Identity transcript *core.Transcript - latestCommit []byte - commits map[string]bool + LatestCommit []byte + commits map[string]int + mutex sync.Mutex } // Init initializes an auditable store func (as *AuditableStore) Init(identity Identity) { as.identity = identity 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 -func (as *AuditableStore) Add(message Message, latestCommit []byte) ([]byte, SignedProof, error) { - if subtle.ConstantTimeCompare(latestCommit, as.latestCommit) == 1 { - as.state.message = append(as.state.message, message) - as.transcript.AddToTranscript("new-message", message) - as.latestCommit = as.identity.Sign(as.transcript.CommitToTranscript("commit")) - return as.latestCommit, as.identity.Sign(as.latestCommit), nil - } - // 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") +func (as *AuditableStore) Add(message Message) SignedProof { + as.mutex.Lock() + defer as.mutex.Unlock() + as.state.Messages = append(as.state.Messages, message) + as.transcript.AddToTranscript("new-message", message) + as.LatestCommit = as.identity.Sign(as.transcript.CommitToTranscript("commit")) + return SignedProof{as.LatestCommit, as.identity.Sign(as.LatestCommit)} } // GetState returns the current auditable state -func (as *AuditableStore) GetState() (State, []byte, SignedProof) { - return as.state, as.latestCommit, as.identity.Sign(as.latestCommit) +func (as *AuditableStore) GetState() (State, SignedProof) { + 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 -func (as *AuditableStore) MergeState(state State, signedStateProof SignedProof, key ed25519.PublicKey) error { - next := len(as.state.message) - for _, m := range state.message[next:] { - as.state.message = append(as.state.message, m) +func (as *AuditableStore) MergeState(state State, signedStateProof SignedProof) error { + next := len(as.state.Messages) + for i, m := range state.Messages[next:] { + as.state.Messages = append(as.state.Messages, m) // We reconstruct the transcript as.transcript.AddToTranscript("new-message", m) - as.latestCommit = as.identity.Sign(as.transcript.CommitToTranscript("commit")) - as.commits[base64.StdEncoding.EncodeToString(as.latestCommit)] = true + as.LatestCommit = as.identity.Sign(as.transcript.CommitToTranscript("commit")) + as.commits[base64.StdEncoding.EncodeToString(as.LatestCommit)] = next + i } // verify that our state matches the servers signed 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. - 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 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. // 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) -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 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 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 { // 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. diff --git a/primitives/auditablestore_test.go b/primitives/auditablestore_test.go index de24f47..2ffe990 100644 --- a/primitives/auditablestore_test.go +++ b/primitives/auditablestore_test.go @@ -12,22 +12,22 @@ func TestAuditableStore(t *testing.T) { as.Init(serverID) vs.Init(serverID) // This doesn't do anything - as.Add([]byte("Hello World"), as.latestCommit) - state, _, proof := as.GetState() + as.Add([]byte("Hello World")) + 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") } - 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. - state, _, proof = as.GetState() - if vs.MergeState(state, proof, serverID.PublicKey()) != nil { + state, proof = as.GetState() + if vs.MergeState(state, proof) != nil { 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 { t.Fatalf("Error validated fraud proof: %v", err) diff --git a/primitives/privacypass/token.go b/primitives/privacypass/token.go index 0935490..c9d6bd6 100644 --- a/primitives/privacypass/token.go +++ b/primitives/privacypass/token.go @@ -5,6 +5,7 @@ import ( "crypto/rand" "cwtch.im/tapir/primitives/core" "fmt" + "git.openprivacy.ca/openprivacy/libricochet-go/log" "github.com/bwesterb/go-ristretto" "golang.org/x/crypto/sha3" ) @@ -29,10 +30,16 @@ type SignedToken struct { // SpentToken encapsulates the parameters needed to spend a Token type SpentToken struct { - t []byte + T []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 // GenToken() & Blind() func (t *Token) GenBlindedToken() BlindedToken { @@ -41,6 +48,7 @@ func (t *Token) GenBlindedToken() BlindedToken { t.r = new(ristretto.Scalar).Rand() Ht := sha3.Sum256(t.t) + log.Debugf("token: %x", Ht) T := new(ristretto.Point).SetElligator(&Ht) P := new(ristretto.Point).ScalarMult(T, t.r) return BlindedToken{P} diff --git a/primitives/privacypass/token_test.go b/primitives/privacypass/token_test.go index 5e3d929..31b69a1 100644 --- a/primitives/privacypass/token_test.go +++ b/primitives/privacypass/token_test.go @@ -2,6 +2,7 @@ package privacypass import ( "cwtch.im/tapir/primitives/core" + "git.openprivacy.ca/openprivacy/libricochet-go/log" "testing" ) @@ -30,6 +31,7 @@ func TestToken_SpendToken(t *testing.T) { } func TestGenerateBlindedTokenBatch(t *testing.T) { + log.SetLevel(log.LevelDebug) server := NewTokenServer() clientTranscript := core.NewTranscript("privacyPass") diff --git a/primitives/privacypass/tokenserver.go b/primitives/privacypass/tokenserver.go index 48a9740..e4d1354 100644 --- a/primitives/privacypass/tokenserver.go +++ b/primitives/privacypass/tokenserver.go @@ -5,21 +5,24 @@ import ( "cwtch.im/tapir/primitives/core" "encoding/hex" "fmt" + "git.openprivacy.ca/openprivacy/libricochet-go/log" "github.com/bwesterb/go-ristretto" "golang.org/x/crypto/sha3" + "sync" ) // TokenServer implements a token server. type TokenServer struct { - k *ristretto.Scalar - Y *ristretto.Point - seen map[string]bool + k *ristretto.Scalar + Y *ristretto.Point + seen map[string]bool + mutex sync.Mutex } // NewTokenServer generates a new TokenServer (used mostly for testing with ephemeral instances) func NewTokenServer() TokenServer { 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 @@ -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. 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 } - Ht := sha3.Sum256(token.t) + Ht := sha3.Sum256(token.T) + log.Debugf("token: %x", Ht) T := new(ristretto.Point).SetElligator(&Ht) 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[:]) K := mac.Sum(data) + log.Debugf("mac: \n%x\nK:%x\n", token.MAC, K) result := hmac.Equal(token.MAC, K) if result == true { - ts.seen[hex.EncodeToString(token.t)] = true + ts.seen[hex.EncodeToString(token.T)] = true } return result }