From 8369ae605b792382733309f2fa812c6c2a9d1b23 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Sun, 15 Sep 2019 14:20:05 -0700 Subject: [PATCH] Adding a PoW Token Service that Grants Tokens for PoW --- .gitignore | 1 + application.go | 12 ++ applications/application_chain.go | 58 ++++++ applications/auth.go | 6 +- applications/auth_test.go | 8 +- applications/proof_of_work_app.go | 102 ++++++++++ applications/token_app.go | 56 +++++ applications/tokenboard.go | 192 ------------------ applications/tokenboard/client.go | 112 ++++++++++ applications/tokenboard/common.go | 52 +++++ applications/tokenboard/server.go | 90 ++++++++ .../tokenboard/tokenboard_integration_test.go | 149 ++++++++++++++ applications/tokenboard_integration_test.go | 118 ----------- applications/transcript_app.go | 7 +- networks/tor/BaseOnionService.go | 2 +- persistence/bolt_persistence.go | 76 +++++++ persistence/bolt_persistence_test.go | 23 +++ persistence/persistence.go | 11 + primitives/{ => auditable}/auditablestore.go | 116 ++++++++--- primitives/auditable/auditablestore_test.go | 70 +++++++ primitives/auditablestore_test.go | 39 ---- primitives/core/transcript.go | 8 + primitives/privacypass/common.go | 17 ++ primitives/privacypass/dlogeq.go | 24 +-- primitives/privacypass/token.go | 11 +- primitives/privacypass/token_test.go | 24 ++- primitives/privacypass/tokenserver.go | 69 +++++-- service.go | 16 +- testing/tests.sh | 3 + 29 files changed, 1037 insertions(+), 435 deletions(-) create mode 100644 applications/application_chain.go create mode 100644 applications/proof_of_work_app.go create mode 100644 applications/token_app.go delete mode 100644 applications/tokenboard.go create mode 100644 applications/tokenboard/client.go create mode 100644 applications/tokenboard/common.go create mode 100644 applications/tokenboard/server.go create mode 100644 applications/tokenboard/tokenboard_integration_test.go delete mode 100644 applications/tokenboard_integration_test.go create mode 100644 persistence/bolt_persistence.go create mode 100644 persistence/bolt_persistence_test.go create mode 100644 persistence/persistence.go rename primitives/{ => auditable}/auditablestore.go (51%) create mode 100644 primitives/auditable/auditablestore_test.go delete mode 100644 primitives/auditablestore_test.go create mode 100644 primitives/privacypass/common.go diff --git a/.gitignore b/.gitignore index 003fb79..e0f8efb 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ vendor/ coverage.out /testing/tor/ /applications/tor/ +*.db diff --git a/application.go b/application.go index 994e78b..1fd47b9 100644 --- a/application.go +++ b/application.go @@ -4,9 +4,21 @@ import ( "cwtch.im/tapir/primitives/core" ) +// Capability defines a status granted to a connection, from an application. That allows the connection to access +// other Application or functions within an Application. +type Capability string + // Application defines the interface for all Tapir Applications type Application interface { NewInstance() Application Init(connection Connection) Transcript() *core.Transcript + PropagateTranscript(transcript *core.Transcript) +} + +// InteractiveApplication defines the interface for interactive Tapir applications (apps that expect the user to send +// and receive messages from) +type InteractiveApplication interface { + Application + Listen() } diff --git a/applications/application_chain.go b/applications/application_chain.go new file mode 100644 index 0000000..3b440af --- /dev/null +++ b/applications/application_chain.go @@ -0,0 +1,58 @@ +package applications + +import ( + "cwtch.im/tapir" +) + +// ApplicationChain is a meta-app that can be used to build complex applications from other applications +type ApplicationChain struct { + TranscriptApp + apps []tapir.Application + endapp tapir.InteractiveApplication + capabilities []tapir.Capability +} + +// ChainApplication adds a new application to the chain. Returns a pointer to app so this call +// can itself be chained. +func (appchain *ApplicationChain) ChainApplication(app tapir.Application, capability tapir.Capability) *ApplicationChain { + appchain.apps = append(appchain.apps, app.NewInstance()) + appchain.capabilities = append(appchain.capabilities, capability) + return appchain +} + +// ChainInteractiveApplication adds an interactive application to the chain. There can only be 1 interactive application. +func (appchain *ApplicationChain) ChainInteractiveApplication(app tapir.InteractiveApplication) *ApplicationChain { + appchain.endapp = app + return appchain +} + +// NewInstance should always return a new instantiation of the application. +func (appchain *ApplicationChain) NewInstance() tapir.Application { + applicationChain := new(ApplicationChain) + for _, app := range appchain.apps { + applicationChain.apps = append(applicationChain.apps, app.NewInstance()) + } + applicationChain.capabilities = appchain.capabilities + return applicationChain +} + +// Init is run when the connection is first started. +func (appchain *ApplicationChain) Init(connection tapir.Connection) { + appchain.TranscriptApp.Init(connection) + for i, app := range appchain.apps { + app.PropagateTranscript(appchain.transcript) + app.Init(connection) + if connection.HasCapability(appchain.capabilities[i]) == false { + connection.Close() + return + } + connection.SetApp(app) + } +} + +// Listen calls listen on the Interactive application +func (appchain *ApplicationChain) Listen() { + if appchain.endapp != nil { + appchain.endapp.Listen() + } +} diff --git a/applications/auth.go b/applications/auth.go index e9da438..8a96544 100644 --- a/applications/auth.go +++ b/applications/auth.go @@ -17,7 +17,7 @@ type AuthMessage struct { } // AuthCapability defines the Authentication Capability granted by AuthApp -const AuthCapability = "AUTH" +const AuthCapability = tapir.Capability("AuthenticationCapability") // AuthApp is the concrete Application type that handles Authentication type AuthApp struct { @@ -83,7 +83,9 @@ func (ea *AuthApp) Init(connection tapir.Connection) { // Derive a challenge from the transcript of the public parameters of this authentication protocol transcript := ea.Transcript() - transcript.AddToTranscript("auth-protocol", []byte(outboundHostname+"-"+inboundHostname)) + transcript.NewProtocol("auth-app") + transcript.AddToTranscript("outbound-hostname", []byte(outboundHostname)) + transcript.AddToTranscript("inbound-hostname", []byte(inboundHostname)) transcript.AddToTranscript("outbound-challenge", outboundAuthMessage) transcript.AddToTranscript("inbound-challenge", inboundAuthMessage) challengeBytes := transcript.CommitToTranscript("3dh-auth-challenge") diff --git a/applications/auth_test.go b/applications/auth_test.go index da1888a..bd5129a 100644 --- a/applications/auth_test.go +++ b/applications/auth_test.go @@ -47,11 +47,11 @@ func (MockConnection) SetHostname(hostname string) { panic("implement me") } -func (MockConnection) HasCapability(name string) bool { +func (MockConnection) HasCapability(name tapir.Capability) bool { panic("implement me") } -func (MockConnection) SetCapability(name string) { +func (MockConnection) SetCapability(name tapir.Capability) { panic("implement me") } @@ -72,6 +72,10 @@ func (MockConnection) App() tapir.Application { return nil } +func (MockConnection) SetApp(tapir.Application) { + // no op +} + func (MockConnection) IsClosed() bool { panic("implement me") } diff --git a/applications/proof_of_work_app.go b/applications/proof_of_work_app.go new file mode 100644 index 0000000..3cf94ee --- /dev/null +++ b/applications/proof_of_work_app.go @@ -0,0 +1,102 @@ +package applications + +import ( + "crypto/sha256" + "cwtch.im/tapir" + "cwtch.im/tapir/primitives/core" + "git.openprivacy.ca/openprivacy/libricochet-go/log" +) + +// ProofOfWorkApplication forces the incoming connection to do proof of work before granting a capability +type ProofOfWorkApplication struct { + TranscriptApp +} + +// transcript constants +const ( + PoWApp = "pow-app" + PoWSeed = "pow-seed" + PoWChallenge = "pow-challenge" + PoWPRNG = "pow-prng" + PoWSolution = "pow-solution" +) + +// SuccessfulProofOfWorkCapability is given when a successfully PoW Challenge has been Completed +const SuccessfulProofOfWorkCapability = tapir.Capability("SuccessfulProofOfWorkCapability") + +// NewInstance should always return a new instantiation of the application. +func (powapp *ProofOfWorkApplication) NewInstance() tapir.Application { + return new(ProofOfWorkApplication) +} + +// Init is run when the connection is first started. +func (powapp *ProofOfWorkApplication) Init(connection tapir.Connection) { + powapp.Transcript().NewProtocol(PoWApp) + if connection.IsOutbound() { + powapp.Transcript().AddToTranscript(PoWSeed, connection.Expect()) + solution := powapp.solveChallenge(powapp.Transcript().CommitToTranscript(PoWChallenge), powapp.transcript.CommitToPRNG(PoWPRNG)) + powapp.transcript.AddToTranscript(PoWSolution, solution) + connection.Send(solution) + connection.SetCapability(SuccessfulProofOfWorkCapability) // We can self grant.because the server will close the connection on failure + return + } + + // We may be the first application, in which case we need to randomize the transcript challenge + // We use the random hostname of the inbound server (if we've authenticated them then the challenge will + // already be sufficiently randomized, so this doesn't hurt) + // It does sadly mean an additional round trip. + powapp.Transcript().AddToTranscript(PoWSeed, []byte(connection.Hostname())) + connection.Send([]byte(connection.Hostname())) + solution := connection.Expect() + challenge := powapp.Transcript().CommitToTranscript(PoWChallenge) + // soft-commitment to the prng, doesn't force the client to use it (but we could technically check that it did, not necessary for the security of this App) + powapp.transcript.CommitToPRNG(PoWPRNG) + powapp.transcript.AddToTranscript(PoWSolution, solution) + if powapp.validateChallenge(challenge, solution) { + connection.SetCapability(SuccessfulProofOfWorkCapability) + return + } +} + +// SolveChallenge takes in a challenge and a message and returns a solution +// The solution is a 24 byte nonce which when hashed with the challenge and the message +// produces a sha256 hash with Difficulty leading 0s +func (powapp *ProofOfWorkApplication) solveChallenge(challenge []byte, prng core.PRNG) []byte { + solved := false + var sum [32]byte + solution := []byte{} + solve := make([]byte, len(challenge)+32) + for !solved { + + solution = prng.Next().Bytes() + + copy(solve[0:], solution[:]) + copy(solve[len(solution):], challenge[:]) + sum = sha256.Sum256(solve) + + solved = true + for i := 0; i < 2; i++ { + if sum[i] != 0x00 { + solved = false + } + } + } + log.Debugf("Validated Challenge %v: %v %v\n", challenge, solution, sum) + return solution[:] +} + +// ValidateChallenge returns true if the message and spamguard pass the challenge +func (powapp *ProofOfWorkApplication) validateChallenge(challenge []byte, solution []byte) bool { + solve := make([]byte, len(challenge)+32) + copy(solve[0:], solution[0:32]) + copy(solve[32:], challenge[:]) + sum := sha256.Sum256(solve) + + for i := 0; i < 2; i++ { + if sum[i] != 0x00 { + return false + } + } + log.Debugf("Validated Challenge %v: %v %v\n", challenge, solution, sum) + return true +} diff --git a/applications/token_app.go b/applications/token_app.go new file mode 100644 index 0000000..80f7245 --- /dev/null +++ b/applications/token_app.go @@ -0,0 +1,56 @@ +package applications + +import ( + "cwtch.im/tapir" + "cwtch.im/tapir/primitives/privacypass" + "encoding/json" + "git.openprivacy.ca/openprivacy/libricochet-go/log" +) + +// TokenApplication provides Tokens for PoW +type TokenApplication struct { + TranscriptApp + TokenService *privacypass.TokenServer + Tokens []*privacypass.Token +} + +// HasTokensCapability is granted once the client has obtained signed tokens +const HasTokensCapability = tapir.Capability("HasTokensCapability") + +// NewInstance should always return a new instantiation of the application. +func (powapp *TokenApplication) NewInstance() tapir.Application { + app := new(TokenApplication) + app.TokenService = powapp.TokenService + return app +} + +// Init is run when the connection is first started. +func (powapp *TokenApplication) Init(connection tapir.Connection) { + powapp.Transcript().NewProtocol("token-app") + if connection.IsOutbound() { + tokens, blinded := privacypass.GenerateBlindedTokenBatch(10) + data, _ := json.Marshal(blinded) + connection.Send(data) + var signedBatch privacypass.SignedBatchWithProof + err := json.Unmarshal(connection.Expect(), &signedBatch) + if err == nil { + verified := privacypass.UnblindSignedTokenBatch(tokens, blinded, signedBatch.SignedTokens, powapp.TokenService.Y, signedBatch.Proof, powapp.Transcript()) + if verified { + log.Debugf("Successfully obtained signed tokens") + powapp.Tokens = tokens + connection.SetCapability(HasTokensCapability) + return + } + log.Debugf("Failed to verify signed token batch") + } + } else { + var blinded []privacypass.BlindedToken + err := json.Unmarshal(connection.Expect(), &blinded) + if err == nil { + batchProof := powapp.TokenService.SignBlindedTokenBatch(blinded, powapp.Transcript()) + data, _ := json.Marshal(batchProof) + connection.Send(data) + return + } + } +} diff --git a/applications/tokenboard.go b/applications/tokenboard.go deleted file mode 100644 index d471e8a..0000000 --- a/applications/tokenboard.go +++ /dev/null @@ -1,192 +0,0 @@ -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/client.go b/applications/tokenboard/client.go new file mode 100644 index 0000000..55fa387 --- /dev/null +++ b/applications/tokenboard/client.go @@ -0,0 +1,112 @@ +package tokenboard + +import ( + "cwtch.im/tapir" + "cwtch.im/tapir/applications" + "cwtch.im/tapir/primitives/auditable" + "cwtch.im/tapir/primitives/privacypass" + "encoding/json" + "git.openprivacy.ca/openprivacy/libricochet-go/log" +) + +// NewTokenBoardClient generates a new Client for Token Board +func NewTokenBoardClient(store *auditable.Store, handler AppHandler, paymentHandler privacypass.TokenPaymentHandler) tapir.Application { + tba := new(Client) + tba.AuditableStore = store + tba.handler = handler + tba.paymentHandler = paymentHandler + return tba +} + +// Client defines a client for the TokenBoard server +type Client struct { + applications.AuthApp + connection tapir.Connection + AuditableStore *auditable.Store + paymentHandler privacypass.TokenPaymentHandler + handler AppHandler +} + +// NewInstance Client a new TokenBoardApp +func (ta *Client) NewInstance() tapir.Application { + tba := new(Client) + tba.AuditableStore = ta.AuditableStore + tba.handler = ta.handler + tba.paymentHandler = ta.paymentHandler + return tba +} + +// Init initializes the cryptographic TokenBoardApp +func (ta *Client) Init(connection tapir.Connection) { + ta.AuthApp.Init(connection) + + if connection.HasCapability(applications.AuthCapability) { + ta.connection = connection + go ta.Listen() + return + } + connection.Close() +} + +// Listen processes the messages for this application +func (ta *Client) Listen() { + for { + log.Debugf("Client waiting...") + data := ta.connection.Expect() + if len(data) == 0 { + log.Debugf("Server closed the connection...") + return // connection is closed + } + + var message Message + json.Unmarshal(data, &message) + log.Debugf("Received a Message: %v", message) + switch message.MessageType { + case postResultMessage: + log.Debugf("Post result: %x", message.PostResult.Proof) + case replayResultMessage: + var state auditable.State + log.Debugf("Replaying %v Messages...", message.ReplayResult.NumMessages) + lastCommit := ta.AuditableStore.LatestCommit + for i := 0; i < message.ReplayResult.NumMessages; i++ { + message := ta.connection.Expect() + state.Messages = append(state.Messages, message) + } + data := ta.connection.Expect() + var signedProof auditable.SignedProof + json.Unmarshal(data, &signedProof) + state.SignedProof = signedProof + err := ta.AuditableStore.AppendState(state) + if err == nil { + log.Debugf("Successfully updated Auditable Store %v", ta.AuditableStore.LatestCommit) + ta.handler.HandleNewMessages(lastCommit) + } else { + log.Debugf("Error updating Auditable Store %v", err) + } + } + } +} + +// Replay posts a Replay Message to the server. +func (ta *Client) Replay() { + log.Debugf("Sending replay request for %v", ta.AuditableStore.LatestCommit) + data, _ := json.Marshal(Message{MessageType: replayRequestMessage, ReplayRequest: replayRequest{LastCommit: ta.AuditableStore.LatestCommit}}) + ta.connection.Send(data) +} + +// PurchaseTokens purchases the given number of tokens from the server (using the provided payment handler) +func (ta *Client) PurchaseTokens() { + ta.paymentHandler.MakePayment() +} + +// Post sends a Post Request to the server +func (ta *Client) Post(message auditable.Message) bool { + token, err := ta.paymentHandler.NextToken(message) + if err == nil { + data, _ := json.Marshal(Message{MessageType: postRequestMessage, PostRequest: postRequest{Token: token, Message: message}}) + ta.connection.Send(data) + return true + } + log.Debugf("No Valid Tokens: %v", err) + return false +} diff --git a/applications/tokenboard/common.go b/applications/tokenboard/common.go new file mode 100644 index 0000000..ba746e1 --- /dev/null +++ b/applications/tokenboard/common.go @@ -0,0 +1,52 @@ +package tokenboard + +import ( + "cwtch.im/tapir/primitives/auditable" + "cwtch.im/tapir/primitives/privacypass" +) + +// AppHandler allows clients to react to specific events. +type AppHandler interface { + HandleNewMessages(previousLastCommit []byte) +} + +// MessageType defines the enum for TokenBoard messages +type messageType int + +const ( + replayRequestMessage messageType = iota + replayResultMessage + postRequestMessage + postResultMessage +) + +// Message encapsulates the application protocol +type Message struct { + MessageType messageType + PostRequest postRequest `json:",omitempty"` + PostResult postResult `json:",omitempty"` + ReplayRequest replayRequest `json:",omitempty"` + ReplayResult replayResult `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 auditable.Message +} + +// PostResult returns the success of a given post attempt +type postResult struct { + Success bool + Proof auditable.SignedProof +} + +// ReplayResult is sent by the server before a stream of replayed messages +type replayResult struct { + NumMessages int +} diff --git a/applications/tokenboard/server.go b/applications/tokenboard/server.go new file mode 100644 index 0000000..764f595 --- /dev/null +++ b/applications/tokenboard/server.go @@ -0,0 +1,90 @@ +package tokenboard + +import ( + "cwtch.im/tapir" + "cwtch.im/tapir/applications" + "cwtch.im/tapir/primitives/auditable" + "cwtch.im/tapir/primitives/privacypass" + "encoding/json" + "git.openprivacy.ca/openprivacy/libricochet-go/log" +) + +// NewTokenBoardServer generates new Server for Token Board +func NewTokenBoardServer(tokenService *privacypass.TokenServer, store *auditable.Store) tapir.Application { + tba := new(Server) + tba.TokenService = tokenService + tba.AuditableStore = store + return tba +} + +// Server defines the token board server +type Server struct { + applications.AuthApp + connection tapir.Connection + TokenService *privacypass.TokenServer + AuditableStore *auditable.Store +} + +// NewInstance creates a new TokenBoardApp +func (ta *Server) NewInstance() tapir.Application { + tba := new(Server) + tba.TokenService = ta.TokenService + tba.AuditableStore = ta.AuditableStore + return tba +} + +// Init initializes the cryptographic TokenBoardApp +func (ta *Server) Init(connection tapir.Connection) { + ta.AuthApp.Init(connection) + + if connection.HasCapability(applications.AuthCapability) { + ta.connection = connection + go ta.Listen() + return + } + connection.Close() +} + +// Listen processes the messages for this application +func (ta *Server) Listen() { + for { + data := ta.connection.Expect() + if len(data) == 0 { + return // connection is closed + } + + var message Message + json.Unmarshal(data, &message) + log.Debugf("Received a Message: %v", message) + switch message.MessageType { + case postRequestMessage: + postrequest := message.PostRequest + log.Debugf("Received a Post Message Request: %x %x", postrequest.Token, postrequest.Message) + ta.postMessageRequest(postrequest.Token, postrequest.Message) + case replayRequestMessage: + log.Debugf("Received Replay Request %v", message.ReplayRequest) + state := ta.AuditableStore.GetStateAfter(message.ReplayRequest.LastCommit) + response, _ := json.Marshal(Message{MessageType: replayResultMessage, ReplayResult: replayResult{len(state.Messages)}}) + log.Debugf("Sending Replay Response %v", replayResult{len(state.Messages)}) + ta.connection.Send(response) + for _, message := range state.Messages { + ta.connection.Send(message) + } + data, _ := json.Marshal(state.SignedProof) + ta.connection.Send(data) + } + } +} + +func (ta *Server) postMessageRequest(token privacypass.SpentToken, message auditable.Message) { + if err := ta.TokenService.SpendToken(token, message); err == nil { + log.Debugf("Token is valid") + signedproof := ta.AuditableStore.Add(message) + data, _ := json.Marshal(Message{MessageType: postResultMessage, PostResult: postResult{true, signedproof}}) + ta.connection.Send(data) + } else { + log.Debugf("Attempt to spend an invalid token: %v", err) + data, _ := json.Marshal(Message{MessageType: postResultMessage, PostResult: postResult{false, auditable.SignedProof{}}}) + ta.connection.Send(data) + } +} diff --git a/applications/tokenboard/tokenboard_integration_test.go b/applications/tokenboard/tokenboard_integration_test.go new file mode 100644 index 0000000..9c8b99e --- /dev/null +++ b/applications/tokenboard/tokenboard_integration_test.go @@ -0,0 +1,149 @@ +package tokenboard + +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" + "git.openprivacy.ca/openprivacy/libricochet-go/connectivity" + "git.openprivacy.ca/openprivacy/libricochet-go/log" + "runtime" + "sync" + "testing" + "time" +) + +type Handler struct { + Store *auditable.Store +} + +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 + ACN connectivity.ACN + ServerHostname string +} + +func (fph *FreePaymentHandler) MakePayment() { + id, sk := primitives.InitializeEphemeralIdentity() + var client tapir.Service + client = new(tor.BaseOnionService) + client.Init(fph.ACN, sk, &id) + + tokenApplication := new(applications.TokenApplication) + tokenApplication.TokenService = fph.TokenService + powTokenApp := new(applications.ApplicationChain). + ChainApplication(new(applications.ProofOfWorkApplication), applications.SuccessfulProofOfWorkCapability). + ChainApplication(tokenApplication, applications.HasTokensCapability) + client.Connect(fph.ServerHostname, powTokenApp) + client.WaitForCapabilityOrClose(fph.ServerHostname, applications.HasTokensCapability) + conn, _ := client.GetConnection(fph.ServerHostname) + powtapp, _ := conn.App().(*applications.TokenApplication) + fph.tokens = append(fph.tokens, powtapp.Tokens...) + log.Debugf("Transcript: %v", powtapp.Transcript().OutputTranscriptToAudit()) + conn.Close() +} + +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(auditable.Store) + serverAuditableStore.Init(sid) + + clientAuditableStore := new(auditable.Store) + // Only initialize with public parameters + sidpubk := sid.PublicKey() + publicsid := primitives.InitializeIdentity("server", nil, &sidpubk) + clientAuditableStore.Init(publicsid) + + // 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() + }() + + // Init the Server running the PoW Token App. + var powTokenService tapir.Service + powTokenService = new(tor.BaseOnionService) + spowid, spowk := primitives.InitializeEphemeralIdentity() + powTokenService.Init(acn, spowk, &spowid) + sg.Add(1) + go func() { + tokenApplication := new(applications.TokenApplication) + tokenApplication.TokenService = &tokenService + powTokenApp := new(applications.ApplicationChain). + ChainApplication(new(applications.ProofOfWorkApplication), applications.SuccessfulProofOfWorkCapability). + ChainApplication(tokenApplication, applications.HasTokensCapability) + powTokenService.Listen(powTokenApp) + sg.Done() + }() + + time.Sleep(time.Second * 30) // wait for server to initialize + 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{ACN: acn, TokenService: &tokenService, ServerHostname: spowid.Hostname()})) + client.WaitForCapabilityOrClose(sid.Hostname(), applications.AuthCapability) + conn, _ := client.GetConnection(sid.Hostname()) + tba, _ := conn.App().(*Client) + tba.PurchaseTokens() + 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() + time.Sleep(time.Second * 10) // We have to wait for the async replay request! + 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() + time.Sleep(time.Second * 10) // We have to wait for the async replay request! + + if tba.Post([]byte("HELLO 11")) { + t.Errorf("Post should have failed.") + } + + time.Sleep(time.Second * 10) + acn.Close() + sg.Wait() +} diff --git a/applications/tokenboard_integration_test.go b/applications/tokenboard_integration_test.go deleted file mode 100644 index 285585d..0000000 --- a/applications/tokenboard_integration_test.go +++ /dev/null @@ -1,118 +0,0 @@ -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/applications/transcript_app.go b/applications/transcript_app.go index 1b6c8ad..151df5d 100644 --- a/applications/transcript_app.go +++ b/applications/transcript_app.go @@ -5,7 +5,7 @@ import ( "cwtch.im/tapir/primitives/core" ) -// TranscriptApp defines a Tapir Meta=App which provides a global cryptographic transcript +// TranscriptApp defines a Tapir Meta-App which provides a global cryptographic transcript type TranscriptApp struct { transcript *core.Transcript } @@ -25,3 +25,8 @@ func (ta *TranscriptApp) Init(connection tapir.Connection) { func (ta *TranscriptApp) Transcript() *core.Transcript { return ta.transcript } + +// PropagateTranscript overrides the default transcript and propagates a transcript from a previous session +func (ta *TranscriptApp) PropagateTranscript(transcript *core.Transcript) { + ta.transcript = transcript +} diff --git a/networks/tor/BaseOnionService.go b/networks/tor/BaseOnionService.go index 5d1a3ec..6a401cb 100644 --- a/networks/tor/BaseOnionService.go +++ b/networks/tor/BaseOnionService.go @@ -34,7 +34,7 @@ func (s *BaseOnionService) Init(acn connectivity.ACN, sk ed25519.PrivateKey, id // WaitForCapabilityOrClose blocks until the connection has the given capability or the underlying connection is closed // (through error or user action) -func (s *BaseOnionService) WaitForCapabilityOrClose(cid string, name string) (tapir.Connection, error) { +func (s *BaseOnionService) WaitForCapabilityOrClose(cid string, name tapir.Capability) (tapir.Connection, error) { conn, err := s.GetConnection(cid) if err == nil { for { diff --git a/persistence/bolt_persistence.go b/persistence/bolt_persistence.go new file mode 100644 index 0000000..5cc382f --- /dev/null +++ b/persistence/bolt_persistence.go @@ -0,0 +1,76 @@ +package persistence + +import ( + "encoding/json" + "git.openprivacy.ca/openprivacy/libricochet-go/log" + bolt "go.etcd.io/bbolt" +) + +// BoltPersistence creates a persistence services backed by an on-disk bolt database +type BoltPersistence struct { + db *bolt.DB +} + +// Open opens a database +func (bp *BoltPersistence) Open(handle string) error { + db, err := bolt.Open(handle, 0600, nil) + bp.db = db + log.Debugf("Loaded the Database") + return err +} + +// Setup initializes the given buckets if they do not exist in the database +func (bp *BoltPersistence) Setup(buckets []string) error { + return bp.db.Update(func(tx *bolt.Tx) error { + for _, bucket := range buckets { + tx.CreateBucketIfNotExists([]byte(bucket)) + } + return nil + }) +} + +// Close closes the databases +func (bp *BoltPersistence) Close() { + bp.db.Close() +} + +// Persist stores a record in the database +func (bp *BoltPersistence) Persist(bucket string, name string, value interface{}) error { + valueBytes, _ := json.Marshal(value) + return bp.db.Update(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte(bucket)) + b.Put([]byte(name), valueBytes) + return nil + }) +} + +// Check returns true if the record exists in the given bucket. +func (bp *BoltPersistence) Check(bucket string, name string) (bool, error) { + log.Debugf("Checking database: %v %v", bucket, name) + var val []byte + err := bp.db.View(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte(bucket)) + val = b.Get([]byte(name)) + return nil + }) + if err != nil { + return false, err + } else if val != nil { + return true, nil + } + return false, nil +} + +// Load reads a value from a given bucket. +func (bp *BoltPersistence) Load(bucket string, name string, value interface{}) error { + var val []byte + err := bp.db.View(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte(bucket)) + val = b.Get([]byte(name)) + return nil + }) + if err != nil { + return err + } + return json.Unmarshal(val, &value) +} diff --git a/persistence/bolt_persistence_test.go b/persistence/bolt_persistence_test.go new file mode 100644 index 0000000..b757474 --- /dev/null +++ b/persistence/bolt_persistence_test.go @@ -0,0 +1,23 @@ +package persistence + +import ( + "testing" +) + +func TestBoltPersistence_Open(t *testing.T) { + var db Service + db = new(BoltPersistence) + db.Open("test.dbgi") + db.Setup([]string{"tokens"}) + db.Persist("tokens", "random_value", true) + + var exists bool + db.Load("tokens", "random_value", &exists) + + if exists { + t.Logf("Successfully stored: %v", exists) + } else { + t.Fatalf("Failure to store record in DB!") + } + db.Close() +} diff --git a/persistence/persistence.go b/persistence/persistence.go new file mode 100644 index 0000000..0dee4b0 --- /dev/null +++ b/persistence/persistence.go @@ -0,0 +1,11 @@ +package persistence + +// Service provides a consistent interface for interacting with on-disk, in-memory or server-backed storage +type Service interface { + Open(handle string) error + Setup(buckets []string) error + Persist(bucket string, name string, value interface{}) error + Check(bucket string, name string) (bool, error) + Load(bucket string, name string, value interface{}) error + Close() +} diff --git a/primitives/auditablestore.go b/primitives/auditable/auditablestore.go similarity index 51% rename from primitives/auditablestore.go rename to primitives/auditable/auditablestore.go index bd33593..ca0b52d 100644 --- a/primitives/auditablestore.go +++ b/primitives/auditable/auditablestore.go @@ -1,64 +1,113 @@ -package primitives +package auditable import ( + "cwtch.im/tapir/persistence" + "cwtch.im/tapir/primitives" "cwtch.im/tapir/primitives/core" "encoding/base64" "errors" + "git.openprivacy.ca/openprivacy/libricochet-go/log" "golang.org/x/crypto/ed25519" "sync" ) // SignedProof encapsulates a signed proof -type SignedProof struct { - Commit []byte - Proof []byte -} +type SignedProof []byte // Message encapsulates a message for more readable code. type Message []byte // State defines an array of messages. type State struct { - Messages []Message + SignedProof SignedProof + Messages []Message } -// AuditableStore defines a cryptographically secure & auditable transcript of messages sent from multiple +// +const ( + auditableDataStoreProtocol = "auditable-data-store" + newMessage = "new-message" + commit = "commit" + collapse = "collapse" +) + +// Store defines a cryptographically secure & auditable transcript of messages sent from multiple // unrelated clients to a server. -type AuditableStore struct { +type Store struct { state State - identity Identity + identity primitives.Identity transcript *core.Transcript LatestCommit []byte commits map[string]int mutex sync.Mutex + db persistence.Service } // Init initializes an auditable store -func (as *AuditableStore) Init(identity Identity) { +func (as *Store) Init(identity primitives.Identity) { as.identity = identity - as.transcript = core.NewTranscript("auditable-data-store") + as.transcript = core.NewTranscript(auditableDataStoreProtocol) as.commits = make(map[string]int) } +const messageBucket = "auditable-messages" + +// LoadFromStorage initializes an auditable store from a DB +func (as *Store) LoadFromStorage(db persistence.Service) { + db.Setup([]string{messageBucket}) + var messages []Message + db.Load(messageBucket, "messages", &messages) + log.Debugf("Loaded from Database: %v", len(messages)) + for _, message := range messages { + as.add(message) + } + log.Debugf("Loaded %v Messages from the Database", len(messages)) + as.db = db +} + // Add adds a message to the auditable store -func (as *AuditableStore) Add(message Message) SignedProof { +func (as *Store) Add(message Message) SignedProof { + sp := as.add(message) + if as.db != nil { + as.db.Persist(messageBucket, "messages", as.state.Messages) + } + return sp +} + +// Add adds a message to the auditable store +func (as *Store) add(message Message) SignedProof { as.mutex.Lock() defer as.mutex.Unlock() + as.transcript.AddToTranscript(newMessage, message) + as.LatestCommit = as.transcript.CommitToTranscript(commit) + 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)} + as.state.SignedProof = as.identity.Sign(as.LatestCommit) + + as.commits[base64.StdEncoding.EncodeToString(as.LatestCommit)] = len(as.state.Messages) - 1 + return as.state.SignedProof } // GetState returns the current auditable state -func (as *AuditableStore) GetState() (State, SignedProof) { +func (as *Store) GetState() State { as.mutex.Lock() defer as.mutex.Unlock() - return as.state, SignedProof{as.LatestCommit, as.identity.Sign(as.LatestCommit)} + return as.state +} + +// GetStateAfter returns the current auditable state after a given commitment +func (as *Store) GetStateAfter(commitment []byte) State { + if commitment == nil { + return as.GetState() + } + var state State + state.Messages = as.GetMessagesAfter(commitment) + state.SignedProof = as.identity.Sign(as.LatestCommit) + return state } // GetMessagesAfter provides access to messages after the given commit. -func (as *AuditableStore) GetMessagesAfter(latestCommit []byte) []Message { +func (as *Store) GetMessagesAfter(latestCommit []byte) []Message { as.mutex.Lock() defer as.mutex.Unlock() index, ok := as.commits[base64.StdEncoding.EncodeToString(latestCommit)] @@ -70,27 +119,33 @@ func (as *AuditableStore) GetMessagesAfter(latestCommit []byte) []Message { 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) error { +// AppendState merges a given state onto our state, first verifying that the two transcripts align +func (as *Store) AppendState(state State) error { next := len(as.state.Messages) - for i, m := range state.Messages[next:] { + for i, m := range state.Messages { 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.transcript.AddToTranscript(newMessage, m) + as.LatestCommit = as.transcript.CommitToTranscript(commit) + log.Debugf("Adding message %d commit: %x", next+i, as.LatestCommit) 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(as.identity.PublicKey(), as.LatestCommit, signedStateProof.Proof) == false { + if ed25519.Verify(as.identity.PublicKey(), as.LatestCommit, state.SignedProof) == false { return errors.New("state is not consistent, the server is malicious") } return nil } +// MergeState merges a given state onto our state, first verifying that the two transcripts align +func (as *Store) MergeState(state State) error { + return as.AppendState(State{Messages: state.Messages[len(as.state.Messages):], SignedProof: state.SignedProof}) +} + // VerifyFraudProof - the main idea behind this is as follows: // // Every update requires the server to sign, and thus commit to, a transcript @@ -101,16 +156,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(signedFraudProof SignedProof, key ed25519.PublicKey) (bool, error) { +func (as *Store) VerifyFraudProof(fraudCommit []byte, signedFraudProof SignedProof, key ed25519.PublicKey) (bool, error) { - if ed25519.Verify(key, signedFraudProof.Commit, signedFraudProof.Proof) == false { + 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 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(signedFraudProof.Commit)] + _, exists := as.commits[base64.StdEncoding.EncodeToString(fraudCommit)] 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. @@ -121,3 +176,10 @@ func (as *AuditableStore) VerifyFraudProof(signedFraudProof SignedProof, key ed2 return false, nil } + +// Collapse constructs a verifiable proof stating that the server has collapsed the previous history into the current +// root = H(onion) +// L = H(Sign(LatestCommit)) +func (as *Store) Collapse() { + as.LatestCommit = as.identity.Sign(as.transcript.CommitToTranscript(collapse)) +} diff --git a/primitives/auditable/auditablestore_test.go b/primitives/auditable/auditablestore_test.go new file mode 100644 index 0000000..f316fb4 --- /dev/null +++ b/primitives/auditable/auditablestore_test.go @@ -0,0 +1,70 @@ +package auditable + +import ( + "cwtch.im/tapir/persistence" + "cwtch.im/tapir/primitives" + "fmt" + "git.openprivacy.ca/openprivacy/libricochet-go/log" + "os" + "testing" +) + +func BenchmarkAuditableStore(b *testing.B) { + log.SetLevel(log.LevelDebug) + os.Remove("benchmark-auditablestore.db") + + as := new(Store) + serverID, _ := primitives.InitializeEphemeralIdentity() + as.Init(serverID) + + db := new(persistence.BoltPersistence) + db.Open("benchmark-auditablestore.db") + + as.LoadFromStorage(db) + + for i := 0; i < b.N; i++ { + data := fmt.Sprintf("Message %v", i) + as.Add(Message(data)) + } + db.Close() + db.Open("benchmark-auditablestore.db") + vs := new(Store) + vs.Init(serverID) + vs.LoadFromStorage(db) + db.Close() + os.Remove("benchmark-auditablestore.db") +} + +func TestAuditableStore(t *testing.T) { + as := new(Store) + vs := new(Store) + + serverID, _ := primitives.InitializeEphemeralIdentity() + as.Init(serverID) + vs.Init(serverID) // This doesn't do anything + + as.Add([]byte("Hello World")) + state := as.GetState() + + if vs.MergeState(state) != nil { + t.Fatalf("Fraud Proof Failed on Honest Proof") + } + + fraudProof := as.Add([]byte("Hello World 2")) + + // If you comment these out it simulates a lying server. + state = as.GetState() + if vs.MergeState(state) != nil { + t.Fatalf("Fraud Proof Failed on Honest Proof") + } + + fraud, err := vs.VerifyFraudProof(as.LatestCommit, fraudProof, serverID.PublicKey()) + + if err != nil { + t.Fatalf("Error validated fraud proof: %v", err) + } + + if fraud { + t.Fatalf("Technically a fraud, but the client hasn't updated yet") + } +} diff --git a/primitives/auditablestore_test.go b/primitives/auditablestore_test.go deleted file mode 100644 index 2ffe990..0000000 --- a/primitives/auditablestore_test.go +++ /dev/null @@ -1,39 +0,0 @@ -package primitives - -import ( - "testing" -) - -func TestAuditableStore(t *testing.T) { - as := new(AuditableStore) - vs := new(AuditableStore) - - serverID, _ := InitializeEphemeralIdentity() - as.Init(serverID) - vs.Init(serverID) // This doesn't do anything - - as.Add([]byte("Hello World")) - state, proof := as.GetState() - - if vs.MergeState(state, proof) != nil { - t.Fatalf("Fraud Proof Failed on Honest Proof") - } - - 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) != nil { - t.Fatalf("Fraud Proof Failed on Honest Proof") - } - - fraud, err := vs.VerifyFraudProof(fraudProof, serverID.PublicKey()) - - if err != nil { - t.Fatalf("Error validated fraud proof: %v", err) - } - - if fraud { - t.Fatalf("Technically a fraud, but the client hasn't updated yet") - } -} diff --git a/primitives/core/transcript.go b/primitives/core/transcript.go index f2d6450..aa49589 100644 --- a/primitives/core/transcript.go +++ b/primitives/core/transcript.go @@ -44,6 +44,14 @@ func (t Transcript) OutputTranscriptToAudit() string { return t.transcript } +// NewProtocol provides explicit protocol separation in a transcript (more readable audit scripts and even more explicit +// binding of committed values to a given context) +func (t *Transcript) NewProtocol(label string) { + op := fmt.Sprintf("---- new-protcol: %s ----", label) + t.transcript = fmt.Sprintf("%v\n%v", t.transcript, op) + t.hash.Write([]byte(op)) +} + // CommitToTranscript generates a challenge based on the current transcript, it also commits the challenge to the transcript. func (t *Transcript) CommitToTranscript(label string) []byte { t.AddToTranscript("commit", []byte(label)) diff --git a/primitives/privacypass/common.go b/primitives/privacypass/common.go new file mode 100644 index 0000000..3f6c91e --- /dev/null +++ b/primitives/privacypass/common.go @@ -0,0 +1,17 @@ +package privacypass + +// Transcript Constants +const ( + BatchProofProtocol = "privacy-pass-batch-proof" + BatchProofX = "X-batch" + BatchProofY = "Y-batch" + BatchProofPVector = "P-vector" + BatchProofQVector = "Q-vector" + + DLEQX = "X" + DLEQY = "Y" + DLEQP = "P" + DLEQQ = "Q" + DLEQA = "A" + DLEQB = "B" +) diff --git a/primitives/privacypass/dlogeq.go b/primitives/privacypass/dlogeq.go index 89caa1e..d6469af 100644 --- a/primitives/privacypass/dlogeq.go +++ b/primitives/privacypass/dlogeq.go @@ -26,12 +26,12 @@ func DiscreteLogEquivalenceProof(k *ristretto.Scalar, X *ristretto.Point, Y *ris A := new(ristretto.Point).ScalarMult(X, t) B := new(ristretto.Point).ScalarMult(P, t) - transcript.AddToTranscript("X", X.Bytes()) - transcript.AddToTranscript("Y", Y.Bytes()) - transcript.AddToTranscript("P", P.Bytes()) - transcript.AddToTranscript("Q", Q.Bytes()) - transcript.AddToTranscript("A", A.Bytes()) - transcript.AddToTranscript("B", B.Bytes()) + transcript.AddToTranscript(DLEQX, X.Bytes()) + transcript.AddToTranscript(DLEQY, Y.Bytes()) + transcript.AddToTranscript(DLEQP, P.Bytes()) + transcript.AddToTranscript(DLEQQ, Q.Bytes()) + transcript.AddToTranscript(DLEQA, A.Bytes()) + transcript.AddToTranscript(DLEQB, B.Bytes()) c := transcript.CommitToTranscriptScalar("c") s := new(ristretto.Scalar).Sub(t, new(ristretto.Scalar).Mul(c, k)) @@ -58,12 +58,12 @@ func VerifyDiscreteLogEquivalenceProof(dleq DLEQProof, X *ristretto.Point, Y *ri A := new(ristretto.Point).Add(Xs, Yc) B := new(ristretto.Point).Add(Ps, Qc) - transcript.AddToTranscript("X", X.Bytes()) - transcript.AddToTranscript("Y", Y.Bytes()) - transcript.AddToTranscript("P", P.Bytes()) - transcript.AddToTranscript("Q", Q.Bytes()) - transcript.AddToTranscript("A", A.Bytes()) - transcript.AddToTranscript("B", B.Bytes()) + transcript.AddToTranscript(DLEQX, X.Bytes()) + transcript.AddToTranscript(DLEQY, Y.Bytes()) + transcript.AddToTranscript(DLEQP, P.Bytes()) + transcript.AddToTranscript(DLEQQ, Q.Bytes()) + transcript.AddToTranscript(DLEQA, A.Bytes()) + transcript.AddToTranscript(DLEQB, B.Bytes()) return transcript.CommitToTranscriptScalar("c").Equals(dleq.C) } diff --git a/primitives/privacypass/token.go b/primitives/privacypass/token.go index c9d6bd6..8958276 100644 --- a/primitives/privacypass/token.go +++ b/primitives/privacypass/token.go @@ -36,7 +36,7 @@ type SpentToken struct { // TokenPaymentHandler defines an interface with external payment processors type TokenPaymentHandler interface { - MakePayment(int) + MakePayment() NextToken(data []byte) (SpentToken, error) } @@ -77,10 +77,11 @@ func GenerateBlindedTokenBatch(num int) (tokens []*Token, blindedTokens []Blinde // verifyBatchProof verifies a given batch proof (see also UnblindSignedTokenBatch) func verifyBatchProof(dleq DLEQProof, Y *ristretto.Point, blindedTokens []BlindedToken, signedTokens []SignedToken, transcript *core.Transcript) bool { - transcript.AddToTranscript("X", new(ristretto.Point).SetBase().Bytes()) - transcript.AddToTranscript("Y", Y.Bytes()) - transcript.AddToTranscript("P[]", []byte(fmt.Sprintf("%v", blindedTokens))) - transcript.AddToTranscript("Q[]", []byte(fmt.Sprintf("%v", signedTokens))) + transcript.NewProtocol(BatchProofProtocol) + transcript.AddToTranscript(BatchProofX, new(ristretto.Point).SetBase().Bytes()) + transcript.AddToTranscript(BatchProofY, Y.Bytes()) + transcript.AddToTranscript(BatchProofPVector, []byte(fmt.Sprintf("%v", blindedTokens))) + transcript.AddToTranscript(BatchProofQVector, []byte(fmt.Sprintf("%v", signedTokens))) prng := transcript.CommitToPRNG("w") M := new(ristretto.Point).SetZero() Z := new(ristretto.Point).SetZero() diff --git a/primitives/privacypass/token_test.go b/primitives/privacypass/token_test.go index 31b69a1..0c1287a 100644 --- a/primitives/privacypass/token_test.go +++ b/primitives/privacypass/token_test.go @@ -1,6 +1,7 @@ package privacypass import ( + "cwtch.im/tapir/persistence" "cwtch.im/tapir/primitives/core" "git.openprivacy.ca/openprivacy/libricochet-go/log" "testing" @@ -17,30 +18,32 @@ func TestToken_SpendToken(t *testing.T) { spentToken := token.SpendToken([]byte("Hello")) - if server.IsValid(spentToken, []byte("Hello World")) == true { + if server.SpendToken(spentToken, []byte("Hello World")) == nil { t.Errorf("Token Should be InValid") } - if server.IsValid(spentToken, []byte("Hello")) == false { - t.Errorf("Token Should be Valid") + if err := server.SpendToken(spentToken, []byte("Hello")); err != nil { + t.Errorf("Token Should be Valid: %v", err) } - if server.IsValid(spentToken, []byte("Hello")) == true { + if err := server.SpendToken(spentToken, []byte("Hello")); err == nil { t.Errorf("Token Should be Spent") } } func TestGenerateBlindedTokenBatch(t *testing.T) { log.SetLevel(log.LevelDebug) - server := NewTokenServer() + db := new(persistence.BoltPersistence) + db.Open("tokens.db") + server := NewTokenServerFromStore(db) clientTranscript := core.NewTranscript("privacyPass") serverTranscript := core.NewTranscript("privacyPass") tokens, blindedTokens := GenerateBlindedTokenBatch(10) - signedTokens, proof := server.SignBlindedTokenBatch(blindedTokens, serverTranscript) + batchProof := server.SignBlindedTokenBatch(blindedTokens, serverTranscript) - verified := UnblindSignedTokenBatch(tokens, blindedTokens, signedTokens, server.Y, proof, clientTranscript) + verified := UnblindSignedTokenBatch(tokens, blindedTokens, batchProof.SignedTokens, server.Y, batchProof.Proof, clientTranscript) if !verified { t.Errorf("Something went wrong, the proof did not pass") @@ -49,8 +52,8 @@ func TestGenerateBlindedTokenBatch(t *testing.T) { // Attempt to Spend All the tokens for _, token := range tokens { spentToken := token.SpendToken([]byte("Hello")) - if server.IsValid(spentToken, []byte("Hello")) == false { - t.Errorf("Token Should be Valid") + if err := server.SpendToken(spentToken, []byte("Hello")); err != nil { + t.Errorf("Token Should be Valid: %v", err) } } @@ -58,8 +61,9 @@ func TestGenerateBlindedTokenBatch(t *testing.T) { t.Logf("Server Transcript,: %s", serverTranscript.OutputTranscriptToAudit()) wrongTranscript := core.NewTranscript("wrongTranscript") - verified = UnblindSignedTokenBatch(tokens, blindedTokens, signedTokens, server.Y, proof, wrongTranscript) + verified = UnblindSignedTokenBatch(tokens, blindedTokens, batchProof.SignedTokens, server.Y, batchProof.Proof, wrongTranscript) if verified { t.Errorf("Something went wrong, the proof passed with wrong transcript: %s", wrongTranscript.OutputTranscriptToAudit()) } + db.Close() } diff --git a/primitives/privacypass/tokenserver.go b/primitives/privacypass/tokenserver.go index e4d1354..421fc72 100644 --- a/primitives/privacypass/tokenserver.go +++ b/primitives/privacypass/tokenserver.go @@ -2,10 +2,10 @@ package privacypass import ( "crypto/hmac" + "cwtch.im/tapir/persistence" "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" @@ -13,16 +13,32 @@ import ( // TokenServer implements a token server. type TokenServer struct { - k *ristretto.Scalar - Y *ristretto.Point - seen map[string]bool - mutex sync.Mutex + k *ristretto.Scalar + Y *ristretto.Point + seen map[string]bool + persistanceService persistence.Service + mutex sync.Mutex } +// SignedBatchWithProof encapsulates a signed batch of blinded tokens with a batch proof for verification +type SignedBatchWithProof struct { + SignedTokens []SignedToken + Proof DLEQProof +} + +const tokenBucket = "tokens" + // 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), sync.Mutex{}} + return TokenServer{k, new(ristretto.Point).ScalarMultBase(k), make(map[string]bool), nil, sync.Mutex{}} +} + +// NewTokenServerFromStore generates a new TokenServer backed by a persistence service. +func NewTokenServerFromStore(persistenceService persistence.Service) TokenServer { + k := new(ristretto.Scalar).Rand() + persistenceService.Setup([]string{tokenBucket}) + return TokenServer{k, new(ristretto.Point).ScalarMultBase(k), make(map[string]bool), persistenceService, sync.Mutex{}} } // SignBlindedToken calculates kP for the given BlindedToken P @@ -32,19 +48,21 @@ func (ts *TokenServer) SignBlindedToken(bt BlindedToken) SignedToken { } // SignBlindedTokenBatch signs a batch of blinded tokens under a given transcript -func (ts *TokenServer) SignBlindedTokenBatch(blindedTokens []BlindedToken, transcript *core.Transcript) (signedTokens []SignedToken, proof DLEQProof) { +func (ts *TokenServer) SignBlindedTokenBatch(blindedTokens []BlindedToken, transcript *core.Transcript) SignedBatchWithProof { + var signedTokens []SignedToken for _, bt := range blindedTokens { signedTokens = append(signedTokens, ts.SignBlindedToken(bt)) } - return signedTokens, ts.constructBatchProof(blindedTokens, signedTokens, transcript) + return SignedBatchWithProof{signedTokens, ts.constructBatchProof(blindedTokens, signedTokens, transcript)} } // 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 { - transcript.AddToTranscript("X", new(ristretto.Point).SetBase().Bytes()) - transcript.AddToTranscript("Y", ts.Y.Bytes()) - transcript.AddToTranscript("P[]", []byte(fmt.Sprintf("%v", blindedTokens))) - transcript.AddToTranscript("Q[]", []byte(fmt.Sprintf("%v", signedTokens))) + transcript.NewProtocol(BatchProofProtocol) + transcript.AddToTranscript(BatchProofX, new(ristretto.Point).SetBase().Bytes()) + transcript.AddToTranscript(BatchProofY, ts.Y.Bytes()) + transcript.AddToTranscript(BatchProofPVector, []byte(fmt.Sprintf("%v", blindedTokens))) + transcript.AddToTranscript(BatchProofQVector, []byte(fmt.Sprintf("%v", signedTokens))) prng := transcript.CommitToPRNG("w") M := new(ristretto.Point).SetZero() @@ -58,25 +76,34 @@ func (ts *TokenServer) constructBatchProof(blindedTokens []BlindedToken, signedT return DiscreteLogEquivalenceProof(ts.k, new(ristretto.Point).SetBase(), ts.Y, M, Z, transcript) } -// IsValid returns true a SpentToken is valid and has never been spent before, false otherwise. -func (ts *TokenServer) IsValid(token SpentToken, data []byte) bool { - log.Debugf("data: [%s]", data) +// SpendToken returns true a SpentToken is valid and has never been spent before, false otherwise. +func (ts *TokenServer) SpendToken(token SpentToken, data []byte) error { 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 + if ts.persistanceService == nil { + if _, spent := ts.seen[hex.EncodeToString(token.T)]; spent { + return fmt.Errorf("token: %v has already been spent", token) + } + } else { + spent, err := ts.persistanceService.Check(tokenBucket, hex.EncodeToString(token.T)) + if err != nil || spent == true { + return fmt.Errorf("token: %v has already been spent", token) + } } 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()...)) 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 + if ts.persistanceService == nil { + ts.seen[hex.EncodeToString(token.T)] = true + } else { + ts.persistanceService.Persist(tokenBucket, hex.EncodeToString(token.T), true) + } + return nil } - return result + return fmt.Errorf("token: %v is invalid and/or has not been signed by this service", token) } diff --git a/service.go b/service.go index e85333a..f41bb34 100644 --- a/service.go +++ b/service.go @@ -19,7 +19,7 @@ type Service interface { Connect(hostname string, application Application) (bool, error) Listen(application Application) error GetConnection(connectionID string) (Connection, error) - WaitForCapabilityOrClose(connectionID string, capability string) (Connection, error) + WaitForCapabilityOrClose(connectionID string, capability Capability) (Connection, error) Shutdown() } @@ -30,12 +30,13 @@ type Connection interface { ID() *primitives.Identity Expect() []byte SetHostname(hostname string) - HasCapability(name string) bool - SetCapability(name string) + HasCapability(name Capability) bool + SetCapability(name Capability) SetEncryptionKey(key [32]byte) Send(message []byte) Close() App() Application + SetApp(application Application) IsClosed() bool } @@ -76,6 +77,11 @@ func (c *connection) App() Application { return c.app } +// App returns the overarching application using this Connection. +func (c *connection) SetApp(application Application) { + c.app = application +} + // Hostname returns the hostname of the connection (if the connection has not been authorized it will return the // temporary hostname identifier) func (c *connection) Hostname() string { @@ -100,13 +106,13 @@ func (c *connection) SetHostname(hostname string) { } // SetCapability sets a capability on the connection -func (c *connection) SetCapability(name string) { +func (c *connection) SetCapability(name Capability) { log.Debugf("[%v -- %v] Setting Capability %v", c.identity.Hostname(), c.hostname, name) c.capabilities.Store(name, true) } // HasCapability checks if the connection has a given capability -func (c *connection) HasCapability(name string) bool { +func (c *connection) HasCapability(name Capability) bool { _, ok := c.capabilities.Load(name) return ok } diff --git a/testing/tests.sh b/testing/tests.sh index c9818a1..3f79df6 100755 --- a/testing/tests.sh +++ b/testing/tests.sh @@ -3,9 +3,12 @@ set -e pwd go test ${1} -coverprofile=applications.cover.out -v ./applications +go test ${1} -coverprofile=applications.tokenboard.cover.out -v ./applications/tokenboard go test ${1} -coverprofile=primitives.cover.out -v ./primitives +go test ${1} -coverprofile=primitives.auditable.cover.out -v ./primitives/auditable go test ${1} -coverprofile=primitives.core.cover.out -v ./primitives/core go test ${1} -coverprofile=primitives.privacypass.cover.out -v ./primitives/privacypass +go test -bench "BenchmarkAuditableStore" -benchtime 1000x primitives/auditable/*.go echo "mode: set" > coverage.out && cat *.cover.out | grep -v mode: | sort -r | \ awk '{if($1 != last) {print $0;last=$1}}' >> coverage.out rm -rf *.cover.out