diff --git a/.gitignore b/.gitignore index 003fb79..99c6321 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ vendor/ coverage.out /testing/tor/ /applications/tor/ +*.db +/applications/tokenboard/tor/ 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..0a57176 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") @@ -94,14 +96,16 @@ func (ea *AuthApp) Init(connection tapir.Connection) { // Since we have set the encryption key on the connection the connection will encrypt any messages we send with that key // To test that the remote peer has done the same we calculate a challenge hash based on the transcript so far and send it to them + // along with our hostname // We expect the remote to do the same, and compare the two. // If successful we extend our auth capability to the connection and reassert the hostname. // We note that the only successful scenario here requires that the remote peer have successfully derived the same // encryption key and the same transcript challenge. - connection.Send(challengeBytes) + connection.Send(append(challengeBytes, []byte(connection.ID().Hostname())...)) remoteChallenge := connection.Expect() - if subtle.ConstantTimeCompare(challengeBytes, remoteChallenge) == 1 { - connection.SetHostname(utils.GetTorV3Hostname(remoteAuthMessage.LongTermPublicKey)) + assertedHostname := utils.GetTorV3Hostname(remoteAuthMessage.LongTermPublicKey) + if subtle.ConstantTimeCompare(append(challengeBytes, []byte(assertedHostname)...), remoteChallenge) == 1 { + connection.SetHostname(assertedHostname) connection.SetCapability(AuthCapability) } else { log.Errorf("Failed Decrypt Challenge: [%x] [%x]\n", remoteChallenge, challengeBytes) 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..697d78f --- /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().Encode(nil) + + 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..1bec236 --- /dev/null +++ b/applications/token_app.go @@ -0,0 +1,58 @@ +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") + log.Debugf(powapp.Transcript().OutputTranscriptToAudit()) + 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()) + log.Debugf(powapp.Transcript().OutputTranscriptToAudit()) + 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..ba75cb3 --- /dev/null +++ b/applications/tokenboard/tokenboard_integration_test.go @@ -0,0 +1,151 @@ +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) + conn,err := client.WaitForCapabilityOrClose(fph.ServerHostname, applications.HasTokensCapability) + if err == nil { + powtapp, _ := conn.App().(*applications.TokenApplication) + fph.tokens = append(fph.tokens, powtapp.Tokens...) + log.Debugf("Transcript: %v", powtapp.Transcript().OutputTranscriptToAudit()) + conn.Close() + } + log.Debugf("Error making payment: %v", err) +} + +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 * 60) // 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/go.mod b/go.mod index 2d5df8d..ecb300a 100644 --- a/go.mod +++ b/go.mod @@ -2,5 +2,11 @@ module cwtch.im/tapir require ( git.openprivacy.ca/openprivacy/libricochet-go v1.0.4 + github.com/gtank/merlin v0.1.1-0.20191105220539-8318aed1a79f + github.com/gtank/ristretto255 v0.1.1-0.20191011164322-af147e8e15b6 + go.etcd.io/bbolt v1.3.3 golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f + golang.org/x/sync v0.0.0-20190423024810-112230192c58 // indirect ) + +go 1.13 diff --git a/go.sum b/go.sum index 4973118..d518b0a 100644 --- a/go.sum +++ b/go.sum @@ -8,11 +8,21 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/gtank/merlin v0.1.1-0.20191105220539-8318aed1a79f h1:8N8XWLZelZNibkhM1FuF+3Ad3YIbgirjdMiVA0eUkaM= +github.com/gtank/merlin v0.1.1-0.20191105220539-8318aed1a79f/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s= +github.com/gtank/ristretto255 v0.1.0 h1:WQKpyRsq8Yt7dm0oq6Gj18BGku/Zbj/TOIolBYfmbiI= +github.com/gtank/ristretto255 v0.1.0/go.mod h1:Ph5OpO6c7xKUGROZfWVLiJf9icMDwUeIvY4OmlYW69o= +github.com/gtank/ristretto255 v0.1.1-0.20191011164322-af147e8e15b6 h1:fYrfnLiiWLCPvmmKbH8AlOwZAtrV0QDKox1HsAEjygY= +github.com/gtank/ristretto255 v0.1.1-0.20191011164322-af147e8e15b6/go.mod h1:Ph5OpO6c7xKUGROZfWVLiJf9icMDwUeIvY4OmlYW69o= +github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643 h1:hLDRPB66XQT/8+wG9WsDpiCvZf1yKO7sz7scAjSlBa0= +github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643/go.mod h1:43+3pMjjKimDBf5Kr4ZFNGbLql1zKkbImw+fZbw3geM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= golang.org/x/crypto v0.0.0-20190128193316-c7b33c32a30b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f h1:R423Cnkcp5JABoeemiGEPlt9tHXFfw5kvc0yqlxRPWo= @@ -20,8 +30,9 @@ golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8U golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 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/operations.go b/primitives/core/operations.go new file mode 100644 index 0000000..61c3bdd --- /dev/null +++ b/primitives/core/operations.go @@ -0,0 +1,186 @@ +package core + +import ( + "fmt" + ristretto "github.com/gtank/ristretto255" +) + +// ScalarVector explicit type checking +type ScalarVector []*ristretto.Scalar + +// PointVector explicit type checking +type PointVector []*ristretto.Element + +// GeneratorVector explicit type checking +type GeneratorVector []*ristretto.Element + +// CopyVector safely copies a vector +func CopyVector(G GeneratorVector) GeneratorVector { + H := make(GeneratorVector, len(G)) + for i, g := range G { + H[i] = new(ristretto.Element).Add(new(ristretto.Element).Zero(), g) + } + return H +} + +// InnerProduct takes the inner product of a and b i.e. +func InnerProduct(a, b ScalarVector) *ristretto.Scalar { + if len(a) != len(b) { + panic(fmt.Sprintf("len(a) = %v ; len(b) = %v;", len(a), len(b))) + } + + result := new(ristretto.Scalar).Zero() + for i, ai := range a { + result.Add(result, new(ristretto.Scalar).Multiply(ai, b[i])) + } + return result +} + +// MultiExp takes in a vector of scalars = {a,b,c...} and a vector of generator = {A,B,C...} and outputs +// {aA,bB,cC} +func MultiExp(a ScalarVector, G GeneratorVector) *ristretto.Element { + if len(a) > len(G) { + panic(fmt.Sprintf("len(a) = %v ; len(b) = %v;", len(a), len(G))) + } + result := new(ristretto.Element).Zero() + for i, ai := range a { + aG := new(ristretto.Element).ScalarMult(ai, G[i]) + result = new(ristretto.Element).Add(result, aG) + } + return result +} + +// SafeAppend is defined for a vector of Scalars +func (a ScalarVector) SafeAppend(b *ristretto.Scalar) ScalarVector { + list := make(ScalarVector, len(a)+1) + for i := 0; i < len(a); i++ { + list[i] = a[i] + } + list[len(a)] = b + return list +} + +// Join is defined for a vector of Scalars +func (a ScalarVector) Join(b ScalarVector) ScalarVector { + list := make(ScalarVector, len(a)+len(b)) + for i := 0; i < len(a); i++ { + list[i] = a[i] + } + for i := len(a); i < len(a)+len(b); i++ { + list[i] = b[i-len(a)] + } + return list +} + +// SafeAppend as defined for a vector of Generators +func (a GeneratorVector) SafeAppend(b *ristretto.Element) GeneratorVector { + list := make(GeneratorVector, len(a)+1) + for i := 0; i < len(a); i++ { + list[i] = a[i] + } + list[len(a)] = b + return list +} + +// Join as defined for a vector of Generators +func (a GeneratorVector) Join(b GeneratorVector) GeneratorVector { + list := make(GeneratorVector, len(a)+len(b)) + for i := 0; i < len(a); i++ { + list[i] = a[i] + } + for i := len(a); i < len(a)+len(b); i++ { + list[i] = b[i-len(a)] + } + return list +} + +// VectorAddScalar takes in a vector v = {a,b,c..} and a scalar s and outputs {a+s,b+s,c+s....} +func VectorAddScalar(vector ScalarVector, scalar *ristretto.Scalar) ScalarVector { + result := make(ScalarVector, len(vector)) + for i := range vector { + result[i] = new(ristretto.Scalar) + result[i].Add(vector[i], scalar) + } + return result +} + +// VectorNegate takes in a vector v = {a,b,c..} and a scalar s and outputs {-a,-b,-c} +func VectorNegate(vector ScalarVector) ScalarVector { + result := make(ScalarVector, len(vector)) + for i := range vector { + result[i] = new(ristretto.Scalar).Negate(vector[i]) + } + return result +} + +// VectorMulScalar takes in a vector v = {a,b,c..} and a scalar s and outputs {as,bs,cs....} +func VectorMulScalar(vector ScalarVector, scalar *ristretto.Scalar) ScalarVector { + result := make(ScalarVector, len(vector)) + for i := range vector { + result[i] = new(ristretto.Scalar) + result[i].Multiply(vector[i], scalar) + } + return result +} + +// EntrywiseSum takes the entry wise sum of two vectors +func EntrywiseSum(vector ScalarVector, vector2 ScalarVector) ScalarVector { + result := make(ScalarVector, len(vector)) + for i, v := range vector { + result[i] = new(ristretto.Scalar) + result[i].Add(v, vector2[i]) + } + return result +} + +// EntrywiseSub takes the entry wise subtraction of two vectors +func EntrywiseSub(vector ScalarVector, vector2 ScalarVector) ScalarVector { + result := make(ScalarVector, len(vector)) + for i, v := range vector { + result[i] = new(ristretto.Scalar) + result[i].Subtract(v, vector2[i]) + } + return result +} + +// EntryWiseProduct takes the entry wise product of two vectors +func EntryWiseProduct(vector ScalarVector, vector2 ScalarVector) ScalarVector { + result := make(ScalarVector, len(vector)) + for i, v := range vector { + result[i] = new(ristretto.Scalar) + result[i].Multiply(v, vector2[i]) + } + return result +} + +// One returns a ristretto scalar == 1 +func One() *ristretto.Scalar { + one := new(ristretto.Scalar) + one.Decode([]byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}) + return one +} + +// IdentityVector is a convenience function to generate a vector v = {1,1,1...1} +func IdentityVector(n int) ScalarVector { + result := make(ScalarVector, n) + one := new(ristretto.Scalar) + one.Decode([]byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}) + for i := 0; i < n; i++ { + result[i] = one + } + return result +} + +// PowerVector creates a vector v = {1,x,x^2,x^3..x^n} +func PowerVector(x *ristretto.Scalar, n int) ScalarVector { + result := make(ScalarVector, n) + one := new(ristretto.Scalar) + one.Decode([]byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}) + result[0] = one + result[1] = x + for i := 2; i < n; i++ { + result[i] = new(ristretto.Scalar) + result[i].Multiply(result[i-1], x) + } + return result +} diff --git a/primitives/core/transcript.go b/primitives/core/transcript.go index f2d6450..b05c59e 100644 --- a/primitives/core/transcript.go +++ b/primitives/core/transcript.go @@ -2,32 +2,28 @@ package core import ( "fmt" - "github.com/bwesterb/go-ristretto" + "github.com/gtank/merlin" + ristretto "github.com/gtank/ristretto255" "golang.org/x/crypto/sha3" - "hash" "io" ) -// Transcript implements a transcript of a public coin argument. +// Transcript provides a consistent transcript primitive for our protocols // // We have the following goals: -// - Provide a consisted transcript API for our zero knowledge protocols // - Allow sequential proofs over a common transcript (ensuring a single proof cannot be extracted standalone) -// - produce an auditable human-readable transcript. +// - be able to produce a human-readable transcript for auditing. // // The design of this API was inspired by Merlin: https://docs.rs/crate/merlin/ -// -// At some point we might want to extend this to be compatible with Merlin transcripts, built on STROBE type Transcript struct { - hash hash.Hash - transcript string + merlinTranscript *merlin.Transcript + transcript string } // NewTranscript creates a new Transcript with the given Label, the label should be unique to the application func NewTranscript(label string) *Transcript { transcript := new(Transcript) - transcript.hash = sha3.New256() - transcript.AddToTranscript("protocol", []byte(label)) + transcript.merlinTranscript = merlin.NewTranscript(label) return transcript } @@ -36,7 +32,13 @@ func NewTranscript(label string) *Transcript { func (t *Transcript) AddToTranscript(label string, b []byte) { op := fmt.Sprintf("%s (%d) %x;", label, len(b), b) t.transcript = fmt.Sprintf("%v\n%v", t.transcript, op) - t.hash.Write([]byte(op)) + t.merlinTranscript.AppendMessage([]byte(label), b) +} + +// AddElementToTranscript appends a value to the transcript with the given label +// This binds the given data to the label. +func (t *Transcript) AddElementToTranscript(label string, element *ristretto.Element) { + t.AddToTranscript(label, element.Encode([]byte{})) } // OutputTranscriptToAudit outputs a human-readable copy of the transcript so far. @@ -44,11 +46,18 @@ 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.merlinTranscript.AppendMessage([]byte("protocol"), []byte(label)) +} + // 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)) - b := t.hash.Sum([]byte{}) - t.AddToTranscript(label, b) + b := t.merlinTranscript.ExtractBytes([]byte(label), 64) + t.transcript = fmt.Sprintf("%v\nextract %v: %v", t.transcript, label, b) return b } @@ -59,25 +68,39 @@ type PRNG struct { // Next returns the next "random" scalar from the PRNG func (prng *PRNG) Next() *ristretto.Scalar { - buf := [32]byte{} + buf := [64]byte{} io.ReadFull(prng.prng, buf[:]) - return new(ristretto.Scalar).SetBytes(&buf) + next := new(ristretto.Scalar) + next.FromUniformBytes(buf[:]) + return next } // CommitToPRNG commits the label to the transcript and derives a PRNG from the transcript. func (t *Transcript) CommitToPRNG(label string) PRNG { - t.AddToTranscript("commit-prng", []byte(label)) - b := t.hash.Sum([]byte{}) - t.AddToTranscript(label, b) + b := t.merlinTranscript.ExtractBytes([]byte(label), 64) prng := sha3.NewShake256() prng.Write(b) return PRNG{prng: prng} } +// CommitToGenerator derives a verifiably random generator from the transcript +func (t *Transcript) CommitToGenerator(label string) *ristretto.Element { + c := t.CommitToTranscript(label) + return new(ristretto.Element).FromUniformBytes(c) +} + +// CommitToGenerators derives a set of verifiably random generators from the transcript +func (t *Transcript) CommitToGenerators(label string, n int) (generators []*ristretto.Element) { + for i := 0; i < n; i++ { + generators = append(generators, t.CommitToGenerator(fmt.Sprintf("%v-%d", label, i))) + } + return generators +} + // CommitToTranscriptScalar is a convenience method for CommitToTranscript which returns a ristretto Scalar func (t *Transcript) CommitToTranscriptScalar(label string) *ristretto.Scalar { c := t.CommitToTranscript(label) - cs := [32]byte{} - copy(cs[:], c[:]) - return new(ristretto.Scalar).SetBytes(&cs) + s := new(ristretto.Scalar) + s.FromUniformBytes(c[:]) + return s } diff --git a/primitives/dlog.go b/primitives/dlog.go deleted file mode 100644 index f89b7c0..0000000 --- a/primitives/dlog.go +++ /dev/null @@ -1,71 +0,0 @@ -package primitives - -import ( - "cwtch.im/tapir/primitives/core" - "github.com/bwesterb/go-ristretto" -) - -// DLProof Encapsulates a Discrete Log / Schnorr Proof -// Note that these parameters are read-only. -type DLProof struct { - V, A ristretto.Point - R ristretto.Scalar -} - -// DiscreteLogProof - Proof of Knowledge of Exponent -// Given V = xG -// Peggy: z := choose randomly from Zq -// A := zG -// c := H(transcript(G,V,A)) mod q -// r := (z + cx) mod q -// -// Sends A,r,V to Vicky -func DiscreteLogProof(x ristretto.Scalar, v ristretto.Point, transcript *core.Transcript) (proof DLProof) { - - transcript.AddToTranscript("G", new(ristretto.Point).SetBase().Bytes()) - - // We bind the proof to our public V - proof.V = v - transcript.AddToTranscript("V", proof.V.Bytes()) - - // Generate a random z - // A := zG - z := new(ristretto.Scalar).Rand() - proof.A = *new(ristretto.Point).ScalarMultBase(z) - transcript.AddToTranscript("A", proof.A.Bytes()) - - // Derive Challenge - c := transcript.CommitToTranscriptScalar("c") - - // r := (z + cx) mod p - cx := new(ristretto.Scalar).Mul(c, &x) - proof.R = *new(ristretto.Scalar).Add(z, cx) - - return -} - -// VerifyDiscreteLogProof validates a given Schnorr Proof -// Vicky gets A,r,V from Peggy -// Vicky computes c := H(transcript(G,V,A)) mod q -// Vicky checks rG := A + cV -// rG ?= zG + cV -// (z+cx)G ?= zG + cV -// ?= zG + cxG -// Thus demonstrating that Peggy knows the discrete log to V -func VerifyDiscreteLogProof(proof DLProof, transcript *core.Transcript) bool { - - transcript.AddToTranscript("G", new(ristretto.Point).SetBase().Bytes()) - transcript.AddToTranscript("V", proof.V.Bytes()) - transcript.AddToTranscript("A", proof.A.Bytes()) - c := transcript.CommitToTranscriptScalar("c") - - // Compute left hand side - lhs := new(ristretto.Point).ScalarMultBase(&proof.R) - - // Compute right hand side - cV := new(ristretto.Point).ScalarMult(&proof.V, c) - rhs := new(ristretto.Point).Add(&proof.A, cV) - - // Result of verification: lhs ?= rhs - return lhs.Equals(rhs) -} 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..e4108ab 100644 --- a/primitives/privacypass/dlogeq.go +++ b/primitives/privacypass/dlogeq.go @@ -1,12 +1,13 @@ package privacypass import ( + "crypto/rand" "cwtch.im/tapir/primitives/core" - "github.com/bwesterb/go-ristretto" + ristretto "github.com/gtank/ristretto255" ) // DLEQProof encapsulates a Chaum-Pedersen DLEQ Proof -// David Chaum and Torben P. Pedersen. Wallet databaseswith observers. In Ernest F. Brickell, editor,CRYPTO’92,volume 740 ofLNCS, pages 89–105. Springer, Heidelberg,August 1993 +//gut In Ernest F. Brickell, editor,CRYPTO’92,volume 740 ofLNCS, pages 89–105. Springer, Heidelberg,August 1993 type DLEQProof struct { C *ristretto.Scalar S *ristretto.Scalar @@ -21,20 +22,23 @@ type DLEQProof struct { // s := (t + ck) mod q // // Sends c,s to Vicky -func DiscreteLogEquivalenceProof(k *ristretto.Scalar, X *ristretto.Point, Y *ristretto.Point, P *ristretto.Point, Q *ristretto.Point, transcript *core.Transcript) DLEQProof { - t := new(ristretto.Scalar).Rand() - A := new(ristretto.Point).ScalarMult(X, t) - B := new(ristretto.Point).ScalarMult(P, t) +func DiscreteLogEquivalenceProof(k *ristretto.Scalar, X *ristretto.Element, Y *ristretto.Element, P *ristretto.Element, Q *ristretto.Element, transcript *core.Transcript) DLEQProof { + private := make([]byte, 64) + rand.Read(private) + t := new(ristretto.Scalar) + t.FromUniformBytes(private) + A := new(ristretto.Element).ScalarMult(t, X) + B := new(ristretto.Element).ScalarMult(t, P) - 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.Encode(nil)) + transcript.AddToTranscript(DLEQY, Y.Encode(nil)) + transcript.AddToTranscript(DLEQP, P.Encode(nil)) + transcript.AddToTranscript(DLEQQ, Q.Encode(nil)) + transcript.AddToTranscript(DLEQA, A.Encode(nil)) + transcript.AddToTranscript(DLEQB, B.Encode(nil)) c := transcript.CommitToTranscriptScalar("c") - s := new(ristretto.Scalar).Sub(t, new(ristretto.Scalar).Mul(c, k)) + s := new(ristretto.Scalar).Subtract(t, new(ristretto.Scalar).Multiply(c, k)) return DLEQProof{c, s} } @@ -48,22 +52,22 @@ func DiscreteLogEquivalenceProof(k *ristretto.Scalar, X *ristretto.Point, Y *ris // B' = P'+Q' == sP + cQ ?= sP + ckP == (s+ck)P == tP == B // c' := H(transcript(X,Y,P,Q,A',B')) // Tests c ?= c -func VerifyDiscreteLogEquivalenceProof(dleq DLEQProof, X *ristretto.Point, Y *ristretto.Point, P *ristretto.Point, Q *ristretto.Point, transcript *core.Transcript) bool { +func VerifyDiscreteLogEquivalenceProof(dleq DLEQProof, X *ristretto.Element, Y *ristretto.Element, P *ristretto.Element, Q *ristretto.Element, transcript *core.Transcript) bool { - Xs := new(ristretto.Point).ScalarMult(X, dleq.S) - Yc := new(ristretto.Point).ScalarMult(Y, dleq.C) - Ps := new(ristretto.Point).ScalarMult(P, dleq.S) - Qc := new(ristretto.Point).ScalarMult(Q, dleq.C) + Xs := new(ristretto.Element).ScalarMult(dleq.S, X) + Yc := new(ristretto.Element).ScalarMult(dleq.C, Y) + Ps := new(ristretto.Element).ScalarMult(dleq.S, P) + Qc := new(ristretto.Element).ScalarMult(dleq.C, Q) - A := new(ristretto.Point).Add(Xs, Yc) - B := new(ristretto.Point).Add(Ps, Qc) + A := new(ristretto.Element).Add(Xs, Yc) + B := new(ristretto.Element).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.Encode(nil)) + transcript.AddToTranscript(DLEQY, Y.Encode(nil)) + transcript.AddToTranscript(DLEQP, P.Encode(nil)) + transcript.AddToTranscript(DLEQQ, Q.Encode(nil)) + transcript.AddToTranscript(DLEQA, A.Encode(nil)) + transcript.AddToTranscript(DLEQB, B.Encode(nil)) - return transcript.CommitToTranscriptScalar("c").Equals(dleq.C) + return transcript.CommitToTranscriptScalar("c").Equal(dleq.C) == 1 } diff --git a/primitives/privacypass/token.go b/primitives/privacypass/token.go index c9d6bd6..4dc8829 100644 --- a/primitives/privacypass/token.go +++ b/primitives/privacypass/token.go @@ -6,7 +6,8 @@ import ( "cwtch.im/tapir/primitives/core" "fmt" "git.openprivacy.ca/openprivacy/libricochet-go/log" - "github.com/bwesterb/go-ristretto" + ristretto "github.com/gtank/ristretto255" + "golang.org/x/crypto/sha3" ) @@ -15,17 +16,17 @@ import ( type Token struct { t []byte r *ristretto.Scalar - W *ristretto.Point + W *ristretto.Element } // BlindedToken encapsulates a Blinded Token type BlindedToken struct { - P *ristretto.Point + P *ristretto.Element } // SignedToken encapsulates a Signed (Blinded) Token type SignedToken struct { - Q *ristretto.Point + Q *ristretto.Element } // SpentToken encapsulates the parameters needed to spend a Token @@ -36,7 +37,7 @@ type SpentToken struct { // TokenPaymentHandler defines an interface with external payment processors type TokenPaymentHandler interface { - MakePayment(int) + MakePayment() NextToken(data []byte) (SpentToken, error) } @@ -45,23 +46,26 @@ type TokenPaymentHandler interface { func (t *Token) GenBlindedToken() BlindedToken { t.t = make([]byte, 32) rand.Read(t.t) - t.r = new(ristretto.Scalar).Rand() + t.r = new(ristretto.Scalar) + b := make([]byte, 64) + rand.Read(b) + t.r.FromUniformBytes(b) - Ht := sha3.Sum256(t.t) + Ht := sha3.Sum512(t.t) log.Debugf("token: %x", Ht) - T := new(ristretto.Point).SetElligator(&Ht) - P := new(ristretto.Point).ScalarMult(T, t.r) + T := new(ristretto.Element).FromUniformBytes(Ht[:]) + P := new(ristretto.Element).ScalarMult(t.r, T) return BlindedToken{P} } // unblindSignedToken unblinds a token that has been signed by a server func (t *Token) unblindSignedToken(token SignedToken) { - t.W = new(ristretto.Point).ScalarMult(token.Q, new(ristretto.Scalar).Inverse(t.r)) + t.W = new(ristretto.Element).ScalarMult(new(ristretto.Scalar).Invert(t.r), token.Q) } // SpendToken binds the token with data and then redeems the token func (t *Token) SpendToken(data []byte) SpentToken { - key := sha3.Sum256(append(t.t, t.W.Bytes()...)) + key := sha3.Sum256(append(t.t, t.W.Encode(nil)...)) mac := hmac.New(sha3.New512, key[:]) return SpentToken{t.t, mac.Sum(data)} } @@ -76,27 +80,29 @@ 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))) +func verifyBatchProof(dleq DLEQProof, Y *ristretto.Element, blindedTokens []BlindedToken, signedTokens []SignedToken, transcript *core.Transcript) bool { + transcript.NewProtocol(BatchProofProtocol) + transcript.AddToTranscript(BatchProofX, new(ristretto.Element).Base().Encode(nil)) + transcript.AddToTranscript(BatchProofY, Y.Encode(nil)) + 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() + M := new(ristretto.Element).Zero() + Z := new(ristretto.Element).Zero() for i := range blindedTokens { c := prng.Next() - M = new(ristretto.Point).Add(new(ristretto.Point).ScalarMult(blindedTokens[i].P, c), M) - Z = new(ristretto.Point).Add(new(ristretto.Point).ScalarMult(signedTokens[i].Q, c), Z) + M = new(ristretto.Element).Add(new(ristretto.Element).ScalarMult(c, blindedTokens[i].P), M) + Z = new(ristretto.Element).Add(new(ristretto.Element).ScalarMult(c, signedTokens[i].Q), Z) } - return VerifyDiscreteLogEquivalenceProof(dleq, new(ristretto.Point).SetBase(), Y, M, Z, transcript) + return VerifyDiscreteLogEquivalenceProof(dleq, new(ristretto.Element).Base(), Y, M, Z, transcript) } // UnblindSignedTokenBatch taking in a set of tokens, their blinded & signed counterparts, a server public key (Y), a DLEQ proof and a transcript // verifies that the signing procedure has taken place correctly and unblinds the tokens. -func UnblindSignedTokenBatch(tokens []*Token, blindedTokens []BlindedToken, signedTokens []SignedToken, Y *ristretto.Point, proof DLEQProof, transcript *core.Transcript) bool { +func UnblindSignedTokenBatch(tokens []*Token, blindedTokens []BlindedToken, signedTokens []SignedToken, Y *ristretto.Element, proof DLEQProof, transcript *core.Transcript) bool { verified := verifyBatchProof(proof, Y, blindedTokens, signedTokens, transcript) if !verified { + log.Debugf(transcript.OutputTranscriptToAudit()) return false } for i, t := range tokens { diff --git a/primitives/privacypass/token_test.go b/primitives/privacypass/token_test.go index 31b69a1..9a23596 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) + db := new(persistence.BoltPersistence) + db.Open("tokens.db") server := NewTokenServer() 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..053400e 100644 --- a/primitives/privacypass/tokenserver.go +++ b/primitives/privacypass/tokenserver.go @@ -2,81 +2,115 @@ package privacypass import ( "crypto/hmac" + "crypto/rand" + "cwtch.im/tapir/persistence" "cwtch.im/tapir/primitives/core" "encoding/hex" "fmt" - "git.openprivacy.ca/openprivacy/libricochet-go/log" - "github.com/bwesterb/go-ristretto" + ristretto "github.com/gtank/ristretto255" "golang.org/x/crypto/sha3" "sync" ) // 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.Element + 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{}} + k := new(ristretto.Scalar) + b := make([]byte, 64) + rand.Read(b) + k.FromUniformBytes(b) + return TokenServer{k, new(ristretto.Element).ScalarBaseMult(k), make(map[string]bool), nil, sync.Mutex{}} +} + +// NewTokenServerFromStore generates a new TokenServer backed by a persistence service. +func NewTokenServerFromStore(persistenceService persistence.Service) TokenServer { + k := new(ristretto.Scalar) + b := make([]byte, 64) + rand.Read(b) + k.FromUniformBytes(b) + persistenceService.Setup([]string{tokenBucket}) + return TokenServer{k, new(ristretto.Element).ScalarBaseMult(k), make(map[string]bool), persistenceService, sync.Mutex{}} } // SignBlindedToken calculates kP for the given BlindedToken P func (ts *TokenServer) SignBlindedToken(bt BlindedToken) SignedToken { - Q := new(ristretto.Point).ScalarMult(bt.P, ts.k) + Q := new(ristretto.Element).ScalarMult(ts.k, bt.P) return SignedToken{Q} } // 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.Element).Base().Encode(nil)) + transcript.AddToTranscript(BatchProofY, ts.Y.Encode(nil)) + 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() + M := new(ristretto.Element).Zero() + Z := new(ristretto.Element).Zero() for i := range blindedTokens { c := prng.Next() - M = new(ristretto.Point).Add(new(ristretto.Point).ScalarMult(blindedTokens[i].P, c), M) - Z = new(ristretto.Point).Add(new(ristretto.Point).ScalarMult(signedTokens[i].Q, c), Z) + M = new(ristretto.Element).Add(new(ristretto.Element).ScalarMult(c, blindedTokens[i].P), M) + Z = new(ristretto.Element).Add(new(ristretto.Element).ScalarMult(c, signedTokens[i].Q), Z) } - return DiscreteLogEquivalenceProof(ts.k, new(ristretto.Point).SetBase(), ts.Y, M, Z, transcript) + return DiscreteLogEquivalenceProof(ts.k, new(ristretto.Element).Base(), 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()...)) + Ht := sha3.Sum512(token.T) + T := new(ristretto.Element).FromUniformBytes(Ht[:]) + W := new(ristretto.Element).ScalarMult(ts.k, T) + key := sha3.Sum256(append(token.T, W.Encode(nil)...)) 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..72645ee 100644 --- a/service.go +++ b/service.go @@ -9,7 +9,6 @@ import ( "golang.org/x/crypto/ed25519" "golang.org/x/crypto/nacl/secretbox" "io" - "net" "sync" ) @@ -19,7 +18,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,19 +29,20 @@ 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 } // Connection defines a Tapir Connection type connection struct { hostname string - conn net.Conn + conn io.ReadWriteCloser capabilities sync.Map encrypted bool key [32]byte @@ -54,7 +54,7 @@ type connection struct { } // NewConnection creates a new Connection -func NewConnection(id *primitives.Identity, hostname string, outbound bool, conn net.Conn, app Application) Connection { +func NewConnection(id *primitives.Identity, hostname string, outbound bool, conn io.ReadWriteCloser, app Application) Connection { connection := new(connection) connection.hostname = hostname connection.conn = conn @@ -76,6 +76,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 +105,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 } @@ -142,9 +147,12 @@ func (c *connection) Expect() []byte { return []byte{} } } - len, _ := binary.Uvarint(buffer[0:2]) + length, _ := binary.Uvarint(buffer[0:2]) + if length+2 >= uint64(c.MaxLength) { + return []byte{} + } //cplog.Debugf("[%v -> %v] Wire Receive: (%d) %x", c.hostname, c.ID.Hostname(), len, buffer) - return buffer[2 : len+2] + return buffer[2 : length+2] } // SetEncryptionKey turns on application-level encryption on the connection using the given key. 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