diff --git a/.drone.yml b/.drone.yml index 1543cc8..6e5bf4f 100644 --- a/.drone.yml +++ b/.drone.yml @@ -43,7 +43,7 @@ pipeline: commands: - ./tor -f ./torrc - sleep 15 - - go test -v cwtch.im/tapir/testing + - go test -race -v cwtch.im/tapir/testing notify-email: image: drillster/drone-email host: build.openprivacy.ca diff --git a/.gitignore b/.gitignore index 4ede555..e1e7b51 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,7 @@ vendor/ /tor/ coverage.out /testing/tor/ +/applications/tor/ +*.db +/applications/tokenboard/tor/ +fuzzing/ diff --git a/application.go b/application.go index 505ac1f..1fd47b9 100644 --- a/application.go +++ b/application.go @@ -1,7 +1,24 @@ package tapir +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 5763232..ab34d42 100644 --- a/applications/auth.go +++ b/applications/auth.go @@ -17,10 +17,11 @@ 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 { + TranscriptApp } // NewInstance creates a new instance of the AuthApp @@ -30,7 +31,8 @@ func (ea AuthApp) NewInstance() tapir.Application { // Init runs the entire AuthApp protocol, at the end of the protocol either the connection is granted AUTH capability // or the connection is closed. -func (ea AuthApp) Init(connection tapir.Connection) { +func (ea *AuthApp) Init(connection tapir.Connection) { + ea.TranscriptApp.Init(connection) longTermPubKey := ed25519.PublicKey(connection.ID().PublicKeyBytes()) ephemeralIdentity, _ := primitives.InitializeEphemeralIdentity() authMessage := AuthMessage{LongTermPublicKey: longTermPubKey, EphemeralPublicKey: ephemeralIdentity.PublicKey()} @@ -61,7 +63,7 @@ func (ea AuthApp) Init(connection tapir.Connection) { challengeRemote, _ := json.Marshal(remoteAuthMessage) challengeLocal, _ := json.Marshal(authMessage) - // Define canonical labels so both sides of the + // Define canonical labels so both sides of the connection can generate the same key var outboundAuthMessage []byte var outboundHostname string var inboundAuthMessage []byte @@ -80,8 +82,10 @@ func (ea AuthApp) Init(connection tapir.Connection) { } // Derive a challenge from the transcript of the public parameters of this authentication protocol - var transcript *primitives.Transcript - transcript = primitives.NewTranscript("tapir-auth-" + outboundHostname + "-" + inboundHostname) + transcript := ea.Transcript() + 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") @@ -92,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..f46fc2a --- /dev/null +++ b/applications/token_app.go @@ -0,0 +1,64 @@ +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") +const numTokens = 10 + +// NewInstance should always return a new instantiation of the application. +func (tokenapp *TokenApplication) NewInstance() tapir.Application { + app := new(TokenApplication) + app.TokenService = tokenapp.TokenService + return app +} + +// Init is run when the connection is first started. +func (tokenapp *TokenApplication) Init(connection tapir.Connection) { + tokenapp.Transcript().NewProtocol("token-app") + log.Debugf(tokenapp.Transcript().OutputTranscriptToAudit()) + if connection.IsOutbound() { + tokens, blinded := privacypass.GenerateBlindedTokenBatch(numTokens) + 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, tokenapp.TokenService.Y, signedBatch.Proof, tokenapp.Transcript()) + if verified { + log.Debugf("Successfully obtained signed tokens") + tokenapp.Tokens = tokens + connection.SetCapability(HasTokensCapability) + return + } + // This will close the connection by default and no tokens will be available. + // This usecase can be checked by the existing WaitForCapabilityOrClose() function using the HasTokensCapability + // If the connection closes without the HasTokensCapability then the error can be handled by whatever client needs it + log.Debugf("Failed to verify signed token batch") + } + return + } + + // We are the server + var blinded []privacypass.BlindedToken + err := json.Unmarshal(connection.Expect(), &blinded) + if err == nil { + batchProof := tokenapp.TokenService.SignBlindedTokenBatch(blinded, tokenapp.Transcript()) + log.Debugf(tokenapp.Transcript().OutputTranscriptToAudit()) + data, _ := json.Marshal(batchProof) + connection.Send(data) + return + } +} diff --git a/applications/tokenboard/client.go b/applications/tokenboard/client.go new file mode 100644 index 0000000..ba20785 --- /dev/null +++ b/applications/tokenboard/client.go @@ -0,0 +1,111 @@ +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) + 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, ta.connection.Hostname()) + 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..c279031 --- /dev/null +++ b/applications/tokenboard/server.go @@ -0,0 +1,92 @@ +package tokenboard + +// NOTE: This is a sketch implementation, Not suitable for production use. The real auditable store is still being designed. + +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) + + 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, append(message, ta.connection.ID().Hostname()...)); 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..a16427a --- /dev/null +++ b/applications/tokenboard/tokenboard_integration_test.go @@ -0,0 +1,152 @@ +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() + return + } + log.Debugf("Error making payment: %v", err) +} + +func (fph *FreePaymentHandler) NextToken(data []byte, hostname string) (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(append(data, hostname...)), 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/transcript_app.go b/applications/transcript_app.go new file mode 100644 index 0000000..151df5d --- /dev/null +++ b/applications/transcript_app.go @@ -0,0 +1,32 @@ +package applications + +import ( + "cwtch.im/tapir" + "cwtch.im/tapir/primitives/core" +) + +// TranscriptApp defines a Tapir Meta-App which provides a global cryptographic transcript +type TranscriptApp struct { + transcript *core.Transcript +} + +// NewInstance creates a new TranscriptApp +func (TranscriptApp) NewInstance() tapir.Application { + ta := new(TranscriptApp) + return ta +} + +// Init initializes the cryptographic transcript +func (ta *TranscriptApp) Init(connection tapir.Connection) { + ta.transcript = core.NewTranscript("tapir-transcript") +} + +// Transcript returns a pointer to the cryptographic transcript +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..e2a0995 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 + github.com/gtank/ristretto255 v0.1.2 + 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..c5a1a2a 100644 --- a/go.sum +++ b/go.sum @@ -8,11 +8,19 @@ 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 h1:eQ90iG7K9pOhtereWsmyRJ6RAwcP4tHTDBHXNg+u5is= +github.com/gtank/merlin v0.1.1/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s= +github.com/gtank/ristretto255 v0.1.2 h1:JEqUCPA1NvLq5DwYtuzigd7ss8fwbYay9fi4/5uMzcc= +github.com/gtank/ristretto255 v0.1.2/go.mod h1:Ph5OpO6c7xKUGROZfWVLiJf9icMDwUeIvY4OmlYW69o= +github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643 h1:hLDRPB66XQT/8+wG9WsDpiCvZf1yKO7sz7scAjSlBa0= +github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643/go.mod h1:43+3pMjjKimDBf5Kr4ZFNGbLql1zKkbImw+fZbw3geM= github.com/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 +28,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..6b2e620 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 { @@ -54,7 +54,6 @@ func (s *BaseOnionService) WaitForCapabilityOrClose(cid string, name string) (ta func (s *BaseOnionService) GetConnection(hostname string) (tapir.Connection, error) { var conn tapir.Connection s.connections.Range(func(key, value interface{}) bool { - log.Debugf("Checking %v", key) connection := value.(tapir.Connection) if connection.Hostname() == hostname { if !connection.IsClosed() { diff --git a/persistence/bolt_persistence.go b/persistence/bolt_persistence.go new file mode 100644 index 0000000..701cc55 --- /dev/null +++ b/persistence/bolt_persistence.go @@ -0,0 +1,77 @@ +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/auditable/auditablestore.go b/primitives/auditable/auditablestore.go new file mode 100644 index 0000000..8a61440 --- /dev/null +++ b/primitives/auditable/auditablestore.go @@ -0,0 +1,187 @@ +package auditable + +// WARNING NOTE: This is a sketch implementation, Not suitable for production use. The real auditable store is still being designed. + +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 []byte + +// Message encapsulates a message for more readable code. +type Message []byte + +// State defines an array of messages. +type State struct { + SignedProof SignedProof + Messages []Message +} + +// +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 Store struct { + state State + 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 *Store) Init(identity primitives.Identity) { + as.identity = identity + 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 *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.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 *Store) GetState() State { + as.mutex.Lock() + defer as.mutex.Unlock() + 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 *Store) GetMessagesAfter(latestCommit []byte) []Message { + as.mutex.Lock() + defer as.mutex.Unlock() + index, ok := as.commits[base64.StdEncoding.EncodeToString(latestCommit)] + if !ok && len(latestCommit) == 32 { + return []Message{} + } else if len(latestCommit) == 0 { + index = -1 + } + return as.state.Messages[index+1:] +} + +// 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 { + as.state.Messages = append(as.state.Messages, m) + + // We reconstruct the transcript + 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, 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 +// Clients reconstruct the transcript via MergeState, as such clients can keep track of every commit. +// if a client can present a signed transcript commit from the server that other clients do not have, it is proof +// that either 1) they are out of sync with the server or 2) the server is presenting different transcripts to different people +// +// 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 *Store) VerifyFraudProof(fraudCommit []byte, signedFraudProof SignedProof, key ed25519.PublicKey) (bool, error) { + + 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(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. + // There is still a possibility that we are out of sync with the server and that new messages have since been added + // We assume that the caller has first Merged the most recent state. + return true, nil + } + + 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/bloom.go b/primitives/bloom.go deleted file mode 100644 index 3104a81..0000000 --- a/primitives/bloom.go +++ /dev/null @@ -1,62 +0,0 @@ -package primitives - -import ( - "crypto/sha256" - "sync" -) - -// BloomFilter implements a bloom filter -type BloomFilter struct { - B []bool - lock sync.Mutex -} - -// Init constructs a bloom filter of size m -func (bf *BloomFilter) Init(m int16) { - bf.B = make([]bool, m) -} - -// Hash transforms a message to a set of bit flips -// Supports up to m == 65535 -func (bf *BloomFilter) Hash(msg []byte) []int { - hash := sha256.Sum256(msg) - - pos1a := (int(hash[0]) + int(hash[1]) + int(hash[2]) + int(hash[3])) % 0xFF - pos1b := (int(hash[4]) + int(hash[5]) + int(hash[6]) + int(hash[7])) % 0xFF - pos1 := ((pos1a << 8) + pos1b) & (0xFFFF % len(bf.B)) - - pos2a := (int(hash[8]) + int(hash[9]) + int(hash[10]) + int(hash[11])) % 0xFF - pos2b := (int(hash[12]) + int(hash[13]) + int(hash[14]) + int(hash[15])) % 0xFF - pos2 := ((pos2a << 8) + pos2b) & (0xFFFF % len(bf.B)) - - pos3a := (int(hash[16]) + int(hash[17]) + int(hash[18]) + int(hash[19])) % 0xFF - pos3b := (int(hash[20]) + int(hash[21]) + int(hash[22]) + int(hash[23])) % 0xFF - pos3 := ((pos3a << 8) + pos3b) & (0xFFFF % len(bf.B)) - - pos4a := (int(hash[24]) + int(hash[25]) + int(hash[26]) + int(hash[27])) % 0xFF - pos4b := (int(hash[28]) + int(hash[29]) + int(hash[30]) + int(hash[31])) % 0xFF - pos4 := ((pos4a << 8) + pos4b) & (0xFFFF % len(bf.B)) - - return []int{pos1, pos2, pos3, pos4} -} - -// Insert updates the BloomFilter (suitable for concurrent use) -func (bf *BloomFilter) Insert(msg []byte) { - pos := bf.Hash(msg) - bf.lock.Lock() - defer bf.lock.Unlock() - bf.B[pos[0]] = true - bf.B[pos[1]] = true - bf.B[pos[2]] = true - bf.B[pos[3]] = true -} - -// Check returns true if the messages might be in the BloomFilter -// (No false positives, possible false negatives due to the probabilistic nature of the filter) -func (bf *BloomFilter) Check(msg []byte) bool { - pos := bf.Hash(msg) - if bf.B[pos[0]] && bf.B[pos[1]] && bf.B[pos[2]] && bf.B[pos[3]] { - return true - } - return false -} diff --git a/primitives/core/transcript.go b/primitives/core/transcript.go new file mode 100644 index 0000000..b05c59e --- /dev/null +++ b/primitives/core/transcript.go @@ -0,0 +1,106 @@ +package core + +import ( + "fmt" + "github.com/gtank/merlin" + ristretto "github.com/gtank/ristretto255" + "golang.org/x/crypto/sha3" + "io" +) + +// Transcript provides a consistent transcript primitive for our protocols +// +// We have the following goals: +// - Allow sequential proofs over a common transcript (ensuring a single proof cannot be extracted standalone) +// - be able to produce a human-readable transcript for auditing. +// +// The design of this API was inspired by Merlin: https://docs.rs/crate/merlin/ +type Transcript struct { + 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.merlinTranscript = merlin.NewTranscript(label) + return transcript +} + +// AddToTranscript appends a value to the transcript with the given label +// This binds the given data to the label. +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.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. +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 { + b := t.merlinTranscript.ExtractBytes([]byte(label), 64) + t.transcript = fmt.Sprintf("%v\nextract %v: %v", t.transcript, label, b) + return b +} + +// PRNG defines a psuedorandom number generator +type PRNG struct { + prng io.Reader +} + +// Next returns the next "random" scalar from the PRNG +func (prng *PRNG) Next() *ristretto.Scalar { + buf := [64]byte{} + io.ReadFull(prng.prng, 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 { + 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) + s := new(ristretto.Scalar) + s.FromUniformBytes(c[:]) + return s +} diff --git a/primitives/core/transcript_test.go b/primitives/core/transcript_test.go new file mode 100644 index 0000000..7f7016d --- /dev/null +++ b/primitives/core/transcript_test.go @@ -0,0 +1,28 @@ +package core + +import ( + "testing" +) + +func TestNewTranscript(t *testing.T) { + + // Some very basic integrity checking + transcript := NewTranscript("label") + + transcript.AddToTranscript("action", []byte("test data")) + + if transcript.OutputTranscriptToAudit() != transcript.OutputTranscriptToAudit() { + t.Fatalf("Multiple Audit Calls should not impact underlying Transcript") + } + t.Logf("%v", transcript.OutputTranscriptToAudit()) + t.Logf("%v", transcript.CommitToTranscript("first commit")) + t.Logf("%v", transcript.OutputTranscriptToAudit()) + t.Logf("%v", transcript.CommitToTranscript("second commit")) + t.Logf("%v", transcript.OutputTranscriptToAudit()) + + transcript.AddToTranscript("action", []byte("test data")) + + t.Logf("%v", transcript.CommitToTranscript("third commit")) + t.Logf("%v", transcript.OutputTranscriptToAudit()) + +} diff --git a/primitives/identity.go b/primitives/identity.go index 9307510..abb6a4f 100644 --- a/primitives/identity.go +++ b/primitives/identity.go @@ -41,7 +41,7 @@ func (i *Identity) PublicKey() ed25519.PublicKey { return *i.edpubk } -// EDH performs a diffie-helman operation on this identities private key with the given public key. +// EDH performs a diffie-hellman operation on this identities private key with the given public key. func (i *Identity) EDH(key ed25519.PublicKey) []byte { secret := utils.EDH(*i.edpk, key) return secret[:] @@ -51,3 +51,8 @@ func (i *Identity) EDH(key ed25519.PublicKey) []byte { func (i *Identity) Hostname() string { return utils.GetTorV3Hostname(*i.edpubk) } + +// Sign produces a signature for a given message attributable to the given identity +func (i *Identity) Sign(input []byte) []byte { + return ed25519.Sign(*i.edpk, input) +} diff --git a/primitives/identity_test.go b/primitives/identity_test.go new file mode 100644 index 0000000..49f5e23 --- /dev/null +++ b/primitives/identity_test.go @@ -0,0 +1,17 @@ +package primitives + +import ( + "testing" +) + +func TestIdentity_EDH(t *testing.T) { + + id1, _ := InitializeEphemeralIdentity() + id2, _ := InitializeEphemeralIdentity() + + k1 := id1.EDH(id2.PublicKey()) + k2 := id2.EDH(id1.PublicKey()) + + t.Logf("k1: %x\nk2: %x\n", k1, k2) + +} 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 new file mode 100644 index 0000000..c57dafc --- /dev/null +++ b/primitives/privacypass/dlogeq.go @@ -0,0 +1,73 @@ +package privacypass + +import ( + "crypto/rand" + "cwtch.im/tapir/primitives/core" + ristretto "github.com/gtank/ristretto255" +) + +// DLEQProof encapsulates a Chaum-Pedersen DLEQ Proof +//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 +} + +// DiscreteLogEquivalenceProof constructs a valid DLEQProof for the given parameters and transcript +// Given Y = kX & Q = kP +// Peggy: t := choose randomly from Zq +// A := tX +// B := tP +// c := H(transcript(X,Y,P,Q,A,B)) +// s := (t + ck) mod q +// +// Sends c,s to Vicky +func DiscreteLogEquivalenceProof(k *ristretto.Scalar, X *ristretto.Element, Y *ristretto.Element, P *ristretto.Element, Q *ristretto.Element, transcript *core.Transcript) DLEQProof { + 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(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).Subtract(t, new(ristretto.Scalar).Multiply(c, k)) + return DLEQProof{c, s} +} + +// VerifyDiscreteLogEquivalenceProof verifies the DLEQ for the given parameters and transcript +// Given Y = kX & Q = kP and Proof = (c,s) +// Vicky: X' := sX +// Y' := cY +// P' := sP +// Q' := cQ +// A' = X'+Y' == sX + cY ?= sG + ckG == (s+ck)X == tX == A +// 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.Element, Y *ristretto.Element, P *ristretto.Element, Q *ristretto.Element, transcript *core.Transcript) bool { + + 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.Element).Add(Xs, Yc) + B := new(ristretto.Element).Add(Ps, Qc) + + 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").Equal(dleq.C) == 1 +} diff --git a/primitives/privacypass/token.go b/primitives/privacypass/token.go new file mode 100644 index 0000000..70ac785 --- /dev/null +++ b/primitives/privacypass/token.go @@ -0,0 +1,117 @@ +package privacypass + +import ( + "crypto/hmac" + "crypto/rand" + "cwtch.im/tapir/primitives/core" + "fmt" + "git.openprivacy.ca/openprivacy/libricochet-go/log" + ristretto "github.com/gtank/ristretto255" + + "golang.org/x/crypto/sha3" +) + +// Token is an implementation of PrivacyPass +// Davidson A, Goldberg I, Sullivan N, Tankersley G, Valsorda F. Privacy pass: Bypassing internet challenges anonymously. Proceedings on Privacy Enhancing Technologies. 2018 Jun 1;2018(3):164-80. +type Token struct { + t []byte + r *ristretto.Scalar + W *ristretto.Element +} + +// GetT returns the underlying bytes for token for use in constraint proofs. +func (t Token) GetT() []byte { + return t.t +} + +// BlindedToken encapsulates a Blinded Token +type BlindedToken struct { + P *ristretto.Element +} + +// SignedToken encapsulates a Signed (Blinded) Token +type SignedToken struct { + Q *ristretto.Element +} + +// SpentToken encapsulates the parameters needed to spend a Token +type SpentToken struct { + T []byte + MAC []byte +} + +// TokenPaymentHandler defines an interface with external payment processors +type TokenPaymentHandler interface { + MakePayment() + // Next Token + NextToken(data []byte, hostname string) (SpentToken, error) +} + +// GenBlindedToken initializes the Token +// GenToken() & Blind() +func (t *Token) GenBlindedToken() BlindedToken { + t.t = make([]byte, 32) + rand.Read(t.t) + t.r = new(ristretto.Scalar) + b := make([]byte, 64) + rand.Read(b) + t.r.FromUniformBytes(b) + + Ht := sha3.Sum512(t.t) + 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.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.Encode(nil)...)) + mac := hmac.New(sha3.New512, key[:]) + return SpentToken{t.t, mac.Sum(data)} +} + +// GenerateBlindedTokenBatch generates a batch of blinded tokens (and their unblinded equivalents) +func GenerateBlindedTokenBatch(num int) (tokens []*Token, blindedTokens []BlindedToken) { + for i := 0; i < num; i++ { + tokens = append(tokens, new(Token)) + blindedTokens = append(blindedTokens, tokens[i].GenBlindedToken()) + } + return +} + +// verifyBatchProof verifies a given batch proof (see also UnblindSignedTokenBatch) +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.Element).Zero() + Z := new(ristretto.Element).Zero() + for i := range blindedTokens { + c := prng.Next() + 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.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.Element, proof DLEQProof, transcript *core.Transcript) bool { + verified := verifyBatchProof(proof, Y, blindedTokens, signedTokens, transcript) + if !verified { + log.Debugf("Failed to unblind tokens: %v", transcript.OutputTranscriptToAudit()) + return false + } + for i, t := range tokens { + t.unblindSignedToken(signedTokens[i]) + } + return true +} diff --git a/primitives/privacypass/token_test.go b/primitives/privacypass/token_test.go new file mode 100644 index 0000000..cd15b09 --- /dev/null +++ b/primitives/privacypass/token_test.go @@ -0,0 +1,101 @@ +package privacypass + +import ( + "cwtch.im/tapir/persistence" + "cwtch.im/tapir/primitives/core" + "git.openprivacy.ca/openprivacy/libricochet-go/log" + "github.com/gtank/ristretto255" + "golang.org/x/crypto/sha3" + "testing" +) + +func TestToken_SpendToken(t *testing.T) { + server := NewTokenServer() + + token := new(Token) + blindedToken := token.GenBlindedToken() + + signedToken := server.SignBlindedToken(blindedToken) + token.unblindSignedToken(signedToken) + + spentToken := token.SpendToken([]byte("Hello")) + + if server.SpendToken(spentToken, []byte("Hello World")) == nil { + t.Errorf("Token Should be InValid") + } + + if err := server.SpendToken(spentToken, []byte("Hello")); err != nil { + t.Errorf("Token Should be Valid: %v", err) + } + + if err := server.SpendToken(spentToken, []byte("Hello")); err == nil { + t.Errorf("Token Should be Spent") + } +} + +func TestToken_ConstrainToToken(t *testing.T) { + server := NewTokenServer() + + token := new(Token) + blindedToken := token.GenBlindedToken() + + signedToken := server.SignBlindedToken(blindedToken) + token.unblindSignedToken(signedToken) + + spentToken := token.SpendToken([]byte("Hello")) + + if server.SpendToken(spentToken, []byte("Hello World")) == nil { + t.Errorf("Token Should be InValid") + } + + token2 := new(Token) + blindedToken2 := token2.GenBlindedToken() + Ht := sha3.Sum512(token.t) + T := new(ristretto255.Element).FromUniformBytes(Ht[:]) + // Constraint forces T = kW to be part of the batch proof + // And because the batch proof must prove that *all* inputs share the same key and also checks the servers public key + // We get a consistency check for almost free. + signedTokens := server.SignBlindedTokenBatchWithConstraint([]BlindedToken{blindedToken2}, token.t, core.NewTranscript("")) + transcript := core.NewTranscript("") + + // NOTE: For this to work token.t and token.W need to be obtain by the client from known source e.g. a public message board. + t.Logf("Result of constaint proof %v", UnblindSignedTokenBatch([]*Token{token2}, []BlindedToken{blindedToken2, {P: T}}, append(signedTokens.SignedTokens, SignedToken{token.W}), server.Y, signedTokens.Proof, transcript)) + t.Log(transcript.OutputTranscriptToAudit()) +} + +func TestGenerateBlindedTokenBatch(t *testing.T) { + log.SetLevel(log.LevelDebug) + db := new(persistence.BoltPersistence) + db.Open("tokens.db") + defer db.Close() + server := NewTokenServerFromStore(db) + + clientTranscript := core.NewTranscript("privacyPass") + serverTranscript := core.NewTranscript("privacyPass") + + tokens, blindedTokens := GenerateBlindedTokenBatch(10) + batchProof := server.SignBlindedTokenBatch(blindedTokens, serverTranscript) + + verified := UnblindSignedTokenBatch(tokens, blindedTokens, batchProof.SignedTokens, server.Y, batchProof.Proof, clientTranscript) + + if !verified { + t.Errorf("Something went wrong, the proof did not pass") + } + + // Attempt to Spend All the tokens + for _, token := range tokens { + spentToken := token.SpendToken([]byte("Hello")) + if err := server.SpendToken(spentToken, []byte("Hello")); err != nil { + t.Errorf("Token Should be Valid: %v", err) + } + } + + t.Logf("Client Transcript,: %s", clientTranscript.OutputTranscriptToAudit()) + t.Logf("Server Transcript,: %s", serverTranscript.OutputTranscriptToAudit()) + + wrongTranscript := core.NewTranscript("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()) + } +} diff --git a/primitives/privacypass/tokenserver.go b/primitives/privacypass/tokenserver.go new file mode 100644 index 0000000..c5036d2 --- /dev/null +++ b/primitives/privacypass/tokenserver.go @@ -0,0 +1,150 @@ +package privacypass + +import ( + "crypto/hmac" + "crypto/rand" + "cwtch.im/tapir/persistence" + "cwtch.im/tapir/primitives/core" + "encoding/hex" + "fmt" + ristretto "github.com/gtank/ristretto255" + "golang.org/x/crypto/sha3" + "sync" +) + +// TokenServer implements a token server. +type TokenServer struct { + 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 `json:"st"` + Proof DLEQProof `json:"dp"` +} + +const tokenBucket = "tokens" +const keyBucket = "keys" + +// NewTokenServer generates a new TokenServer (used mostly for testing with ephemeral instances) +func NewTokenServer() *TokenServer { + k := new(ristretto.Scalar) + b := make([]byte, 64) + _, err := rand.Read(b) + if err != nil { + // unable to generate secure random numbers + panic("unable to generate secure random numbers") + } + 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 { + tokenServer := NewTokenServer() + persistenceService.Setup([]string{tokenBucket}) + persistenceService.Setup([]string{keyBucket}) + exists, err := persistenceService.Check(keyBucket, "k") + if err != nil { + panic(err) + } + // if we don't have a stored k then save the one we have generated + // otherwise use the k we have stored + if !exists { + persistenceService.Persist(keyBucket, "k", tokenServer.k) + } else { + persistenceService.Load(keyBucket, "k", tokenServer.k) + + // recalculate public key from stored k + tokenServer.Y = new(ristretto.Element).ScalarBaseMult(tokenServer.k) + } + + tokenServer.persistanceService = persistenceService + return tokenServer +} + +// SignBlindedToken calculates kP for the given BlindedToken P +func (ts *TokenServer) SignBlindedToken(bt BlindedToken) SignedToken { + 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) SignedBatchWithProof { + var signedTokens []SignedToken + for _, bt := range blindedTokens { + signedTokens = append(signedTokens, ts.SignBlindedToken(bt)) + } + return SignedBatchWithProof{signedTokens, ts.constructBatchProof(blindedTokens, signedTokens, transcript)} +} + +// SignBlindedTokenBatchWithConstraint signs a batch of blinded tokens under a given transcript given a constraint that the tokens must be signed +// by the same public key as an existing token +func (ts *TokenServer) SignBlindedTokenBatchWithConstraint(blindedTokens []BlindedToken, constraintToken []byte, transcript *core.Transcript) SignedBatchWithProof { + var signedTokens []SignedToken + for _, bt := range blindedTokens { + signedTokens = append(signedTokens, ts.SignBlindedToken(bt)) + } + Ht := sha3.Sum512(constraintToken) + T := new(ristretto.Element).FromUniformBytes(Ht[:]) + // W == kT + W := new(ristretto.Element).ScalarMult(ts.k, T) + blindedTokens = append(blindedTokens, BlindedToken{P: T}) + return SignedBatchWithProof{signedTokens, ts.constructBatchProof(blindedTokens, append(signedTokens, SignedToken{Q: W}), 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.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.Element).Zero() + Z := new(ristretto.Element).Zero() + + for i := range blindedTokens { + c := prng.Next() + 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.Element).Base(), ts.Y, M, Z, transcript) +} + +// 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 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.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[:]) + computedMAC := mac.Sum(data) + result := hmac.Equal(token.MAC, computedMAC) + if result == 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 fmt.Errorf("token: %v is invalid and/or has not been signed by this service", token) +} diff --git a/primitives/time.go b/primitives/time.go deleted file mode 100644 index b957a46..0000000 --- a/primitives/time.go +++ /dev/null @@ -1,21 +0,0 @@ -package primitives - -import ( - "time" -) - -// TimeProvider is an interface used by services to timestamp events. Why not just have them use time.Now()? We want -// to be able to write tests that simulate behavior over several hours, and thus having an interface to abstract away -// time details for the services is very useful. -type TimeProvider interface { - GetCurrentTime() time.Time -} - -// OSTimeProvider provides a wrapper around time provider which simply provides the time as given by the operating system. -type OSTimeProvider struct { -} - -// GetCurrentTime returns the time provided by the OS -func (ostp OSTimeProvider) GetCurrentTime() time.Time { - return time.Now() -} diff --git a/primitives/transcript.go b/primitives/transcript.go deleted file mode 100644 index ebb4bed..0000000 --- a/primitives/transcript.go +++ /dev/null @@ -1,51 +0,0 @@ -package primitives - -import ( - "fmt" - "golang.org/x/crypto/sha3" - "hash" -) - -// Transcript implements a transcript of a public coin argument. -// -// 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. -// -// 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 -} - -// 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)) - return transcript -} - -// AddToTranscript appends a value to the transcript with the given label -// This binds the given data to the label. -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)) -} - -// OutputTranscriptToAudit outputs a human-readable copy of the transcript so far. -func (t Transcript) OutputTranscriptToAudit() string { - return t.transcript -} - -// 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) - return b -} diff --git a/service.go b/service.go index e85333a..bc2a6bd 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 @@ -51,10 +51,11 @@ type connection struct { outbound bool closed bool MaxLength int + lock sync.Mutex } // 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 @@ -73,12 +74,23 @@ func (c *connection) ID() *primitives.Identity { // App returns the overarching application using this Connection. func (c *connection) App() Application { + c.lock.Lock() + defer c.lock.Unlock() return c.app } +// App returns the overarching application using this Connection. +func (c *connection) SetApp(application Application) { + c.lock.Lock() + defer c.lock.Unlock() + 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 { + c.lock.Lock() + defer c.lock.Unlock() return c.hostname } @@ -90,29 +102,35 @@ func (c *connection) IsOutbound() bool { // IsClosed returns true if the connection is closed (connections cannot be reopened) func (c *connection) IsClosed() bool { + c.lock.Lock() + defer c.lock.Unlock() return c.closed } // SetHostname sets the hostname on the connection func (c *connection) SetHostname(hostname string) { + c.lock.Lock() + defer c.lock.Unlock() log.Debugf("[%v -- %v] Asserting Remote Hostname: %v", c.identity.Hostname(), c.hostname, hostname) c.hostname = hostname } // 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 } // Close forcibly closes the connection func (c *connection) Close() { + c.lock.Lock() + defer c.lock.Unlock() c.closed = true c.conn.Close() } @@ -128,7 +146,8 @@ func (c *connection) Expect() []byte { c.closed = true return []byte{} } - + c.lock.Lock() + defer c.lock.Unlock() if c.encrypted { var decryptNonce [24]byte copy(decryptNonce[:], buffer[:24]) @@ -142,13 +161,18 @@ 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. func (c *connection) SetEncryptionKey(key [32]byte) { + c.lock.Lock() + defer c.lock.Unlock() c.key = key c.encrypted = true } @@ -160,6 +184,8 @@ func (c *connection) Send(message []byte) { binary.PutUvarint(buffer[0:2], uint64(len(message))) copy(buffer[2:], message) + c.lock.Lock() + defer c.lock.Unlock() if c.encrypted { var nonce [24]byte if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil { diff --git a/testing/tapir_integration_test.go b/testing/tapir_integration_test.go index 65eae06..e77aa9c 100644 --- a/testing/tapir_integration_test.go +++ b/testing/tapir_integration_test.go @@ -21,12 +21,12 @@ type SimpleApp struct { } // NewInstance should always return a new instantiation of the application. -func (ea SimpleApp) NewInstance() tapir.Application { +func (ea *SimpleApp) NewInstance() tapir.Application { return new(SimpleApp) } // Init is run when the connection is first started. -func (ea SimpleApp) Init(connection tapir.Connection) { +func (ea *SimpleApp) Init(connection tapir.Connection) { // First run the Authentication App ea.AuthApp.Init(connection) @@ -76,7 +76,7 @@ func TestTapir(t *testing.T) { sg := new(sync.WaitGroup) sg.Add(1) go func() { - service.Listen(SimpleApp{}) + service.Listen(new(SimpleApp)) sg.Done() }() @@ -114,7 +114,7 @@ func genclient(acn connectivity.ACN) (tapir.Service, string) { // Client will Connect and launch it's own Echo App goroutine. func connectclient(client tapir.Service, key ed25519.PublicKey, group *sync.WaitGroup) { - client.Connect(utils.GetTorV3Hostname(key), SimpleApp{}) + client.Connect(utils.GetTorV3Hostname(key), new(SimpleApp)) // Once connected, it shouldn't take long to authenticate and run the application. So for the purposes of this demo // we will wait a little while then exit. diff --git a/testing/tapir_malicious_remote_integration_test.go b/testing/tapir_malicious_remote_integration_test.go index e18a56d..4262fff 100644 --- a/testing/tapir_malicious_remote_integration_test.go +++ b/testing/tapir_malicious_remote_integration_test.go @@ -40,7 +40,7 @@ func TestTapirMaliciousRemote(t *testing.T) { sg := new(sync.WaitGroup) sg.Add(1) go func() { - service.Listen(applications.AuthApp{}) + service.Listen(new(applications.AuthApp)) sg.Done() }() @@ -67,7 +67,7 @@ func TestTapirMaliciousRemote(t *testing.T) { // Client will Connect and launch it's own Echo App goroutine. func connectclientandfail(client tapir.Service, key ed25519.PublicKey, group *sync.WaitGroup, t *testing.T) { - client.Connect(utils.GetTorV3Hostname(key), applications.AuthApp{}) + client.Connect(utils.GetTorV3Hostname(key), new(applications.AuthApp)) // Once connected, it shouldn't take long to authenticate and run the application. So for the purposes of this demo // we will wait a little while then exit. diff --git a/testing/tests.sh b/testing/tests.sh index b3e6652..f402dbe 100755 --- a/testing/tests.sh +++ b/testing/tests.sh @@ -2,7 +2,13 @@ set -e pwd -go test ${1} -coverprofile=applications.cover.out -v ./applications +go test -race ${1} -coverprofile=applications.cover.out -v ./applications +go test -race ${1} -coverprofile=applications.tokenboard.cover.out -v ./applications/tokenboard +go test -race ${1} -coverprofile=primitives.cover.out -v ./primitives +go test -race ${1} -coverprofile=primitives.auditable.cover.out -v ./primitives/auditable +go test -race ${1} -coverprofile=primitives.core.cover.out -v ./primitives/core +go test -race ${1} -coverprofile=primitives.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