Adding a PoW Token Service that Grants Tokens for PoW
the build was successful Details

This commit is contained in:
Sarah Jamie Lewis 2019-09-15 14:20:05 -07:00
parent 1066862f58
commit 8369ae605b
29 changed files with 1037 additions and 435 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@ vendor/
coverage.out coverage.out
/testing/tor/ /testing/tor/
/applications/tor/ /applications/tor/
*.db

View File

@ -4,9 +4,21 @@ import (
"cwtch.im/tapir/primitives/core" "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 // Application defines the interface for all Tapir Applications
type Application interface { type Application interface {
NewInstance() Application NewInstance() Application
Init(connection Connection) Init(connection Connection)
Transcript() *core.Transcript 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()
} }

View File

@ -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()
}
}

View File

@ -17,7 +17,7 @@ type AuthMessage struct {
} }
// AuthCapability defines the Authentication Capability granted by AuthApp // 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 // AuthApp is the concrete Application type that handles Authentication
type AuthApp struct { 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 // Derive a challenge from the transcript of the public parameters of this authentication protocol
transcript := ea.Transcript() 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("outbound-challenge", outboundAuthMessage)
transcript.AddToTranscript("inbound-challenge", inboundAuthMessage) transcript.AddToTranscript("inbound-challenge", inboundAuthMessage)
challengeBytes := transcript.CommitToTranscript("3dh-auth-challenge") challengeBytes := transcript.CommitToTranscript("3dh-auth-challenge")

View File

@ -47,11 +47,11 @@ func (MockConnection) SetHostname(hostname string) {
panic("implement me") panic("implement me")
} }
func (MockConnection) HasCapability(name string) bool { func (MockConnection) HasCapability(name tapir.Capability) bool {
panic("implement me") panic("implement me")
} }
func (MockConnection) SetCapability(name string) { func (MockConnection) SetCapability(name tapir.Capability) {
panic("implement me") panic("implement me")
} }
@ -72,6 +72,10 @@ func (MockConnection) App() tapir.Application {
return nil return nil
} }
func (MockConnection) SetApp(tapir.Application) {
// no op
}
func (MockConnection) IsClosed() bool { func (MockConnection) IsClosed() bool {
panic("implement me") panic("implement me")
} }

View File

@ -0,0 +1,102 @@
package applications
import (
"crypto/sha256"
"cwtch.im/tapir"
"cwtch.im/tapir/primitives/core"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
)
// ProofOfWorkApplication forces the incoming connection to do proof of work before granting a capability
type ProofOfWorkApplication struct {
TranscriptApp
}
// transcript constants
const (
PoWApp = "pow-app"
PoWSeed = "pow-seed"
PoWChallenge = "pow-challenge"
PoWPRNG = "pow-prng"
PoWSolution = "pow-solution"
)
// SuccessfulProofOfWorkCapability is given when a successfully PoW Challenge has been Completed
const SuccessfulProofOfWorkCapability = tapir.Capability("SuccessfulProofOfWorkCapability")
// NewInstance should always return a new instantiation of the application.
func (powapp *ProofOfWorkApplication) NewInstance() tapir.Application {
return new(ProofOfWorkApplication)
}
// Init is run when the connection is first started.
func (powapp *ProofOfWorkApplication) Init(connection tapir.Connection) {
powapp.Transcript().NewProtocol(PoWApp)
if connection.IsOutbound() {
powapp.Transcript().AddToTranscript(PoWSeed, connection.Expect())
solution := powapp.solveChallenge(powapp.Transcript().CommitToTranscript(PoWChallenge), powapp.transcript.CommitToPRNG(PoWPRNG))
powapp.transcript.AddToTranscript(PoWSolution, solution)
connection.Send(solution)
connection.SetCapability(SuccessfulProofOfWorkCapability) // We can self grant.because the server will close the connection on failure
return
}
// We may be the first application, in which case we need to randomize the transcript challenge
// We use the random hostname of the inbound server (if we've authenticated them then the challenge will
// already be sufficiently randomized, so this doesn't hurt)
// It does sadly mean an additional round trip.
powapp.Transcript().AddToTranscript(PoWSeed, []byte(connection.Hostname()))
connection.Send([]byte(connection.Hostname()))
solution := connection.Expect()
challenge := powapp.Transcript().CommitToTranscript(PoWChallenge)
// soft-commitment to the prng, doesn't force the client to use it (but we could technically check that it did, not necessary for the security of this App)
powapp.transcript.CommitToPRNG(PoWPRNG)
powapp.transcript.AddToTranscript(PoWSolution, solution)
if powapp.validateChallenge(challenge, solution) {
connection.SetCapability(SuccessfulProofOfWorkCapability)
return
}
}
// SolveChallenge takes in a challenge and a message and returns a solution
// The solution is a 24 byte nonce which when hashed with the challenge and the message
// produces a sha256 hash with Difficulty leading 0s
func (powapp *ProofOfWorkApplication) solveChallenge(challenge []byte, prng core.PRNG) []byte {
solved := false
var sum [32]byte
solution := []byte{}
solve := make([]byte, len(challenge)+32)
for !solved {
solution = prng.Next().Bytes()
copy(solve[0:], solution[:])
copy(solve[len(solution):], challenge[:])
sum = sha256.Sum256(solve)
solved = true
for i := 0; i < 2; i++ {
if sum[i] != 0x00 {
solved = false
}
}
}
log.Debugf("Validated Challenge %v: %v %v\n", challenge, solution, sum)
return solution[:]
}
// ValidateChallenge returns true if the message and spamguard pass the challenge
func (powapp *ProofOfWorkApplication) validateChallenge(challenge []byte, solution []byte) bool {
solve := make([]byte, len(challenge)+32)
copy(solve[0:], solution[0:32])
copy(solve[32:], challenge[:])
sum := sha256.Sum256(solve)
for i := 0; i < 2; i++ {
if sum[i] != 0x00 {
return false
}
}
log.Debugf("Validated Challenge %v: %v %v\n", challenge, solution, sum)
return true
}

56
applications/token_app.go Normal file
View File

@ -0,0 +1,56 @@
package applications
import (
"cwtch.im/tapir"
"cwtch.im/tapir/primitives/privacypass"
"encoding/json"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
)
// TokenApplication provides Tokens for PoW
type TokenApplication struct {
TranscriptApp
TokenService *privacypass.TokenServer
Tokens []*privacypass.Token
}
// HasTokensCapability is granted once the client has obtained signed tokens
const HasTokensCapability = tapir.Capability("HasTokensCapability")
// NewInstance should always return a new instantiation of the application.
func (powapp *TokenApplication) NewInstance() tapir.Application {
app := new(TokenApplication)
app.TokenService = powapp.TokenService
return app
}
// Init is run when the connection is first started.
func (powapp *TokenApplication) Init(connection tapir.Connection) {
powapp.Transcript().NewProtocol("token-app")
if connection.IsOutbound() {
tokens, blinded := privacypass.GenerateBlindedTokenBatch(10)
data, _ := json.Marshal(blinded)
connection.Send(data)
var signedBatch privacypass.SignedBatchWithProof
err := json.Unmarshal(connection.Expect(), &signedBatch)
if err == nil {
verified := privacypass.UnblindSignedTokenBatch(tokens, blinded, signedBatch.SignedTokens, powapp.TokenService.Y, signedBatch.Proof, powapp.Transcript())
if verified {
log.Debugf("Successfully obtained signed tokens")
powapp.Tokens = tokens
connection.SetCapability(HasTokensCapability)
return
}
log.Debugf("Failed to verify signed token batch")
}
} else {
var blinded []privacypass.BlindedToken
err := json.Unmarshal(connection.Expect(), &blinded)
if err == nil {
batchProof := powapp.TokenService.SignBlindedTokenBatch(blinded, powapp.Transcript())
data, _ := json.Marshal(batchProof)
connection.Send(data)
return
}
}
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -0,0 +1,149 @@
package tokenboard
import (
"cwtch.im/tapir"
"cwtch.im/tapir/applications"
"cwtch.im/tapir/networks/tor"
"cwtch.im/tapir/primitives"
"cwtch.im/tapir/primitives/auditable"
"cwtch.im/tapir/primitives/privacypass"
"errors"
"git.openprivacy.ca/openprivacy/libricochet-go/connectivity"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"runtime"
"sync"
"testing"
"time"
)
type Handler struct {
Store *auditable.Store
}
func (h Handler) HandleNewMessages(previousLastCommit []byte) {
log.Debugf("Handling Messages After %x", previousLastCommit)
messages := h.Store.GetMessagesAfter(previousLastCommit)
for _, message := range messages {
log.Debugf("Message %s", message)
}
}
type FreePaymentHandler struct {
tokens []*privacypass.Token
TokenService *privacypass.TokenServer
ACN connectivity.ACN
ServerHostname string
}
func (fph *FreePaymentHandler) MakePayment() {
id, sk := primitives.InitializeEphemeralIdentity()
var client tapir.Service
client = new(tor.BaseOnionService)
client.Init(fph.ACN, sk, &id)
tokenApplication := new(applications.TokenApplication)
tokenApplication.TokenService = fph.TokenService
powTokenApp := new(applications.ApplicationChain).
ChainApplication(new(applications.ProofOfWorkApplication), applications.SuccessfulProofOfWorkCapability).
ChainApplication(tokenApplication, applications.HasTokensCapability)
client.Connect(fph.ServerHostname, powTokenApp)
client.WaitForCapabilityOrClose(fph.ServerHostname, applications.HasTokensCapability)
conn, _ := client.GetConnection(fph.ServerHostname)
powtapp, _ := conn.App().(*applications.TokenApplication)
fph.tokens = append(fph.tokens, powtapp.Tokens...)
log.Debugf("Transcript: %v", powtapp.Transcript().OutputTranscriptToAudit())
conn.Close()
}
func (fph *FreePaymentHandler) NextToken(data []byte) (privacypass.SpentToken, error) {
if len(fph.tokens) == 0 {
return privacypass.SpentToken{}, errors.New("No more tokens")
}
token := fph.tokens[0]
fph.tokens = fph.tokens[1:]
return token.SpendToken(data), nil
}
func TestTokenBoardApp(t *testing.T) {
// numRoutinesStart := runtime.NumGoroutine()
log.SetLevel(log.LevelDebug)
log.Infof("Number of goroutines open at start: %d", runtime.NumGoroutine())
// Connect to Tor
var acn connectivity.ACN
acn, _ = connectivity.StartTor("./", "")
acn.WaitTillBootstrapped()
// Generate Server Key
sid, sk := primitives.InitializeEphemeralIdentity()
tokenService := privacypass.NewTokenServer()
serverAuditableStore := new(auditable.Store)
serverAuditableStore.Init(sid)
clientAuditableStore := new(auditable.Store)
// Only initialize with public parameters
sidpubk := sid.PublicKey()
publicsid := primitives.InitializeIdentity("server", nil, &sidpubk)
clientAuditableStore.Init(publicsid)
// Init the Server running the Simple App.
var service tapir.Service
service = new(tor.BaseOnionService)
service.Init(acn, sk, &sid)
// Goroutine Management
sg := new(sync.WaitGroup)
sg.Add(1)
go func() {
service.Listen(NewTokenBoardServer(&tokenService, serverAuditableStore))
sg.Done()
}()
// Init the Server running the PoW Token App.
var powTokenService tapir.Service
powTokenService = new(tor.BaseOnionService)
spowid, spowk := primitives.InitializeEphemeralIdentity()
powTokenService.Init(acn, spowk, &spowid)
sg.Add(1)
go func() {
tokenApplication := new(applications.TokenApplication)
tokenApplication.TokenService = &tokenService
powTokenApp := new(applications.ApplicationChain).
ChainApplication(new(applications.ProofOfWorkApplication), applications.SuccessfulProofOfWorkCapability).
ChainApplication(tokenApplication, applications.HasTokensCapability)
powTokenService.Listen(powTokenApp)
sg.Done()
}()
time.Sleep(time.Second * 30) // wait for server to initialize
id, sk := primitives.InitializeEphemeralIdentity()
var client tapir.Service
client = new(tor.BaseOnionService)
client.Init(acn, sk, &id)
client.Connect(sid.Hostname(), NewTokenBoardClient(clientAuditableStore, Handler{Store: clientAuditableStore}, &FreePaymentHandler{ACN: acn, TokenService: &tokenService, ServerHostname: spowid.Hostname()}))
client.WaitForCapabilityOrClose(sid.Hostname(), applications.AuthCapability)
conn, _ := client.GetConnection(sid.Hostname())
tba, _ := conn.App().(*Client)
tba.PurchaseTokens()
tba.Post([]byte("HELLO 1"))
tba.Post([]byte("HELLO 2"))
tba.Post([]byte("HELLO 3"))
tba.Post([]byte("HELLO 4"))
tba.Post([]byte("HELLO 5"))
tba.Replay()
time.Sleep(time.Second * 10) // We have to wait for the async replay request!
tba.Post([]byte("HELLO 6"))
tba.Post([]byte("HELLO 7"))
tba.Post([]byte("HELLO 8"))
tba.Post([]byte("HELLO 9"))
tba.Post([]byte("HELLO 10"))
tba.Replay()
time.Sleep(time.Second * 10) // We have to wait for the async replay request!
if tba.Post([]byte("HELLO 11")) {
t.Errorf("Post should have failed.")
}
time.Sleep(time.Second * 10)
acn.Close()
sg.Wait()
}

View File

@ -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)
}

View File

@ -5,7 +5,7 @@ import (
"cwtch.im/tapir/primitives/core" "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 { type TranscriptApp struct {
transcript *core.Transcript transcript *core.Transcript
} }
@ -25,3 +25,8 @@ func (ta *TranscriptApp) Init(connection tapir.Connection) {
func (ta *TranscriptApp) Transcript() *core.Transcript { func (ta *TranscriptApp) Transcript() *core.Transcript {
return ta.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
}

View File

@ -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 // WaitForCapabilityOrClose blocks until the connection has the given capability or the underlying connection is closed
// (through error or user action) // (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) conn, err := s.GetConnection(cid)
if err == nil { if err == nil {
for { for {

View File

@ -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)
}

View File

@ -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()
}

View File

@ -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()
}

View File

@ -1,64 +1,113 @@
package primitives package auditable
import ( import (
"cwtch.im/tapir/persistence"
"cwtch.im/tapir/primitives"
"cwtch.im/tapir/primitives/core" "cwtch.im/tapir/primitives/core"
"encoding/base64" "encoding/base64"
"errors" "errors"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"golang.org/x/crypto/ed25519" "golang.org/x/crypto/ed25519"
"sync" "sync"
) )
// SignedProof encapsulates a signed proof // SignedProof encapsulates a signed proof
type SignedProof struct { type SignedProof []byte
Commit []byte
Proof []byte
}
// Message encapsulates a message for more readable code. // Message encapsulates a message for more readable code.
type Message []byte type Message []byte
// State defines an array of messages. // State defines an array of messages.
type State struct { type State struct {
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. // unrelated clients to a server.
type AuditableStore struct { type Store struct {
state State state State
identity Identity identity primitives.Identity
transcript *core.Transcript transcript *core.Transcript
LatestCommit []byte LatestCommit []byte
commits map[string]int commits map[string]int
mutex sync.Mutex mutex sync.Mutex
db persistence.Service
} }
// Init initializes an auditable store // Init initializes an auditable store
func (as *AuditableStore) Init(identity Identity) { func (as *Store) Init(identity primitives.Identity) {
as.identity = identity as.identity = identity
as.transcript = core.NewTranscript("auditable-data-store") as.transcript = core.NewTranscript(auditableDataStoreProtocol)
as.commits = make(map[string]int) 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 // 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() as.mutex.Lock()
defer as.mutex.Unlock() 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.Messages = append(as.state.Messages, message)
as.transcript.AddToTranscript("new-message", message) as.state.SignedProof = as.identity.Sign(as.LatestCommit)
as.LatestCommit = as.identity.Sign(as.transcript.CommitToTranscript("commit"))
return SignedProof{as.LatestCommit, 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 // GetState returns the current auditable state
func (as *AuditableStore) GetState() (State, SignedProof) { func (as *Store) GetState() State {
as.mutex.Lock() as.mutex.Lock()
defer as.mutex.Unlock() 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. // 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() as.mutex.Lock()
defer as.mutex.Unlock() defer as.mutex.Unlock()
index, ok := as.commits[base64.StdEncoding.EncodeToString(latestCommit)] 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:] return as.state.Messages[index+1:]
} }
// MergeState merges a given state onto our state, first verifying that the two transcripts align // AppendState merges a given state onto our state, first verifying that the two transcripts align
func (as *AuditableStore) MergeState(state State, signedStateProof SignedProof) error { func (as *Store) AppendState(state State) error {
next := len(as.state.Messages) 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) as.state.Messages = append(as.state.Messages, m)
// We reconstruct the transcript // We reconstruct the transcript
as.transcript.AddToTranscript("new-message", m) as.transcript.AddToTranscript(newMessage, m)
as.LatestCommit = as.identity.Sign(as.transcript.CommitToTranscript("commit")) 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 as.commits[base64.StdEncoding.EncodeToString(as.LatestCommit)] = next + i
} }
// verify that our state matches the servers signed state // verify that our state matches the servers signed state
// this is *not* a security check, as a rogue server can simply sign any state // this is *not* a security check, as a rogue server can simply sign any state
// however committing to a state allows us to build fraud proofs for malicious servers later on. // however committing to a state allows us to build fraud proofs for malicious servers later on.
if ed25519.Verify(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 errors.New("state is not consistent, the server is malicious")
} }
return nil 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: // VerifyFraudProof - the main idea behind this is as follows:
// //
// Every update requires the server to sign, and thus commit to, a transcript // 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. // If, after syncing, the FraudProof still validates, then the server must be malicious.
// the information revealed by publicizing a fraud proof is minimal it only reveals the inconsistent transcript commit // the information revealed by publicizing a fraud proof is minimal it only reveals the inconsistent transcript commit
// and not the cause (which could be reordered messages, dropped messages, additional messages or any combination) // and not the cause (which could be reordered messages, dropped messages, additional messages or any combination)
func (as *AuditableStore) VerifyFraudProof(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 due to misuse of this function (trying to verify a proof with the wrong public key)
// This could happen if the server lies to us and submits a fake state proof, however we cannot use this to // This could happen if the server lies to us and submits a fake state proof, however we cannot use this to
// prove that the server is acting maliciously // prove that the server is acting maliciously
return false, errors.New("signed proof has not been signed by the given public key") return false, errors.New("signed proof has not been signed by the given public key")
} }
_, exists := as.commits[base64.StdEncoding.EncodeToString(signedFraudProof.Commit)] _, exists := as.commits[base64.StdEncoding.EncodeToString(fraudCommit)]
if !exists { if !exists {
// We have a message signed by the server which verifies that a message was inserted into the state at a given index // We have a message signed by the server which verifies that a message was inserted into the state at a given index
// However this directly contradicts our version of the state. // However this directly contradicts our version of the state.
@ -121,3 +176,10 @@ func (as *AuditableStore) VerifyFraudProof(signedFraudProof SignedProof, key ed2
return false, 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))
}

View File

@ -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")
}
}

View File

@ -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")
}
}

View File

@ -44,6 +44,14 @@ func (t Transcript) OutputTranscriptToAudit() string {
return t.transcript return t.transcript
} }
// NewProtocol provides explicit protocol separation in a transcript (more readable audit scripts and even more explicit
// binding of committed values to a given context)
func (t *Transcript) NewProtocol(label string) {
op := fmt.Sprintf("---- new-protcol: %s ----", label)
t.transcript = fmt.Sprintf("%v\n%v", t.transcript, op)
t.hash.Write([]byte(op))
}
// CommitToTranscript generates a challenge based on the current transcript, it also commits the challenge to the transcript. // CommitToTranscript generates a challenge based on the current transcript, it also commits the challenge to the transcript.
func (t *Transcript) CommitToTranscript(label string) []byte { func (t *Transcript) CommitToTranscript(label string) []byte {
t.AddToTranscript("commit", []byte(label)) t.AddToTranscript("commit", []byte(label))

View File

@ -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"
)

View File

@ -26,12 +26,12 @@ func DiscreteLogEquivalenceProof(k *ristretto.Scalar, X *ristretto.Point, Y *ris
A := new(ristretto.Point).ScalarMult(X, t) A := new(ristretto.Point).ScalarMult(X, t)
B := new(ristretto.Point).ScalarMult(P, t) B := new(ristretto.Point).ScalarMult(P, t)
transcript.AddToTranscript("X", X.Bytes()) transcript.AddToTranscript(DLEQX, X.Bytes())
transcript.AddToTranscript("Y", Y.Bytes()) transcript.AddToTranscript(DLEQY, Y.Bytes())
transcript.AddToTranscript("P", P.Bytes()) transcript.AddToTranscript(DLEQP, P.Bytes())
transcript.AddToTranscript("Q", Q.Bytes()) transcript.AddToTranscript(DLEQQ, Q.Bytes())
transcript.AddToTranscript("A", A.Bytes()) transcript.AddToTranscript(DLEQA, A.Bytes())
transcript.AddToTranscript("B", B.Bytes()) transcript.AddToTranscript(DLEQB, B.Bytes())
c := transcript.CommitToTranscriptScalar("c") c := transcript.CommitToTranscriptScalar("c")
s := new(ristretto.Scalar).Sub(t, new(ristretto.Scalar).Mul(c, k)) s := new(ristretto.Scalar).Sub(t, new(ristretto.Scalar).Mul(c, k))
@ -58,12 +58,12 @@ func VerifyDiscreteLogEquivalenceProof(dleq DLEQProof, X *ristretto.Point, Y *ri
A := new(ristretto.Point).Add(Xs, Yc) A := new(ristretto.Point).Add(Xs, Yc)
B := new(ristretto.Point).Add(Ps, Qc) B := new(ristretto.Point).Add(Ps, Qc)
transcript.AddToTranscript("X", X.Bytes()) transcript.AddToTranscript(DLEQX, X.Bytes())
transcript.AddToTranscript("Y", Y.Bytes()) transcript.AddToTranscript(DLEQY, Y.Bytes())
transcript.AddToTranscript("P", P.Bytes()) transcript.AddToTranscript(DLEQP, P.Bytes())
transcript.AddToTranscript("Q", Q.Bytes()) transcript.AddToTranscript(DLEQQ, Q.Bytes())
transcript.AddToTranscript("A", A.Bytes()) transcript.AddToTranscript(DLEQA, A.Bytes())
transcript.AddToTranscript("B", B.Bytes()) transcript.AddToTranscript(DLEQB, B.Bytes())
return transcript.CommitToTranscriptScalar("c").Equals(dleq.C) return transcript.CommitToTranscriptScalar("c").Equals(dleq.C)
} }

View File

@ -36,7 +36,7 @@ type SpentToken struct {
// TokenPaymentHandler defines an interface with external payment processors // TokenPaymentHandler defines an interface with external payment processors
type TokenPaymentHandler interface { type TokenPaymentHandler interface {
MakePayment(int) MakePayment()
NextToken(data []byte) (SpentToken, error) NextToken(data []byte) (SpentToken, error)
} }
@ -77,10 +77,11 @@ func GenerateBlindedTokenBatch(num int) (tokens []*Token, blindedTokens []Blinde
// verifyBatchProof verifies a given batch proof (see also UnblindSignedTokenBatch) // verifyBatchProof verifies a given batch proof (see also UnblindSignedTokenBatch)
func verifyBatchProof(dleq DLEQProof, Y *ristretto.Point, blindedTokens []BlindedToken, signedTokens []SignedToken, transcript *core.Transcript) bool { func verifyBatchProof(dleq DLEQProof, Y *ristretto.Point, blindedTokens []BlindedToken, signedTokens []SignedToken, transcript *core.Transcript) bool {
transcript.AddToTranscript("X", new(ristretto.Point).SetBase().Bytes()) transcript.NewProtocol(BatchProofProtocol)
transcript.AddToTranscript("Y", Y.Bytes()) transcript.AddToTranscript(BatchProofX, new(ristretto.Point).SetBase().Bytes())
transcript.AddToTranscript("P[]", []byte(fmt.Sprintf("%v", blindedTokens))) transcript.AddToTranscript(BatchProofY, Y.Bytes())
transcript.AddToTranscript("Q[]", []byte(fmt.Sprintf("%v", signedTokens))) transcript.AddToTranscript(BatchProofPVector, []byte(fmt.Sprintf("%v", blindedTokens)))
transcript.AddToTranscript(BatchProofQVector, []byte(fmt.Sprintf("%v", signedTokens)))
prng := transcript.CommitToPRNG("w") prng := transcript.CommitToPRNG("w")
M := new(ristretto.Point).SetZero() M := new(ristretto.Point).SetZero()
Z := new(ristretto.Point).SetZero() Z := new(ristretto.Point).SetZero()

View File

@ -1,6 +1,7 @@
package privacypass package privacypass
import ( import (
"cwtch.im/tapir/persistence"
"cwtch.im/tapir/primitives/core" "cwtch.im/tapir/primitives/core"
"git.openprivacy.ca/openprivacy/libricochet-go/log" "git.openprivacy.ca/openprivacy/libricochet-go/log"
"testing" "testing"
@ -17,30 +18,32 @@ func TestToken_SpendToken(t *testing.T) {
spentToken := token.SpendToken([]byte("Hello")) 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") t.Errorf("Token Should be InValid")
} }
if server.IsValid(spentToken, []byte("Hello")) == false { if err := server.SpendToken(spentToken, []byte("Hello")); err != nil {
t.Errorf("Token Should be Valid") 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") t.Errorf("Token Should be Spent")
} }
} }
func TestGenerateBlindedTokenBatch(t *testing.T) { func TestGenerateBlindedTokenBatch(t *testing.T) {
log.SetLevel(log.LevelDebug) log.SetLevel(log.LevelDebug)
server := NewTokenServer() db := new(persistence.BoltPersistence)
db.Open("tokens.db")
server := NewTokenServerFromStore(db)
clientTranscript := core.NewTranscript("privacyPass") clientTranscript := core.NewTranscript("privacyPass")
serverTranscript := core.NewTranscript("privacyPass") serverTranscript := core.NewTranscript("privacyPass")
tokens, blindedTokens := GenerateBlindedTokenBatch(10) 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 { if !verified {
t.Errorf("Something went wrong, the proof did not pass") 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 // Attempt to Spend All the tokens
for _, token := range tokens { for _, token := range tokens {
spentToken := token.SpendToken([]byte("Hello")) spentToken := token.SpendToken([]byte("Hello"))
if server.IsValid(spentToken, []byte("Hello")) == false { if err := server.SpendToken(spentToken, []byte("Hello")); err != nil {
t.Errorf("Token Should be Valid") t.Errorf("Token Should be Valid: %v", err)
} }
} }
@ -58,8 +61,9 @@ func TestGenerateBlindedTokenBatch(t *testing.T) {
t.Logf("Server Transcript,: %s", serverTranscript.OutputTranscriptToAudit()) t.Logf("Server Transcript,: %s", serverTranscript.OutputTranscriptToAudit())
wrongTranscript := core.NewTranscript("wrongTranscript") 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 { if verified {
t.Errorf("Something went wrong, the proof passed with wrong transcript: %s", wrongTranscript.OutputTranscriptToAudit()) t.Errorf("Something went wrong, the proof passed with wrong transcript: %s", wrongTranscript.OutputTranscriptToAudit())
} }
db.Close()
} }

View File

@ -2,10 +2,10 @@ package privacypass
import ( import (
"crypto/hmac" "crypto/hmac"
"cwtch.im/tapir/persistence"
"cwtch.im/tapir/primitives/core" "cwtch.im/tapir/primitives/core"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"github.com/bwesterb/go-ristretto" "github.com/bwesterb/go-ristretto"
"golang.org/x/crypto/sha3" "golang.org/x/crypto/sha3"
"sync" "sync"
@ -13,16 +13,32 @@ import (
// TokenServer implements a token server. // TokenServer implements a token server.
type TokenServer struct { type TokenServer struct {
k *ristretto.Scalar k *ristretto.Scalar
Y *ristretto.Point Y *ristretto.Point
seen map[string]bool seen map[string]bool
mutex sync.Mutex 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) // NewTokenServer generates a new TokenServer (used mostly for testing with ephemeral instances)
func NewTokenServer() TokenServer { func NewTokenServer() TokenServer {
k := new(ristretto.Scalar).Rand() k := new(ristretto.Scalar).Rand()
return TokenServer{k, new(ristretto.Point).ScalarMultBase(k), make(map[string]bool), sync.Mutex{}} return TokenServer{k, new(ristretto.Point).ScalarMultBase(k), make(map[string]bool), nil, sync.Mutex{}}
}
// NewTokenServerFromStore generates a new TokenServer backed by a persistence service.
func NewTokenServerFromStore(persistenceService persistence.Service) TokenServer {
k := new(ristretto.Scalar).Rand()
persistenceService.Setup([]string{tokenBucket})
return TokenServer{k, new(ristretto.Point).ScalarMultBase(k), make(map[string]bool), persistenceService, sync.Mutex{}}
} }
// SignBlindedToken calculates kP for the given BlindedToken P // SignBlindedToken calculates kP for the given BlindedToken P
@ -32,19 +48,21 @@ func (ts *TokenServer) SignBlindedToken(bt BlindedToken) SignedToken {
} }
// SignBlindedTokenBatch signs a batch of blinded tokens under a given transcript // 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 { for _, bt := range blindedTokens {
signedTokens = append(signedTokens, ts.SignBlindedToken(bt)) 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 // 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 { func (ts *TokenServer) constructBatchProof(blindedTokens []BlindedToken, signedTokens []SignedToken, transcript *core.Transcript) DLEQProof {
transcript.AddToTranscript("X", new(ristretto.Point).SetBase().Bytes()) transcript.NewProtocol(BatchProofProtocol)
transcript.AddToTranscript("Y", ts.Y.Bytes()) transcript.AddToTranscript(BatchProofX, new(ristretto.Point).SetBase().Bytes())
transcript.AddToTranscript("P[]", []byte(fmt.Sprintf("%v", blindedTokens))) transcript.AddToTranscript(BatchProofY, ts.Y.Bytes())
transcript.AddToTranscript("Q[]", []byte(fmt.Sprintf("%v", signedTokens))) transcript.AddToTranscript(BatchProofPVector, []byte(fmt.Sprintf("%v", blindedTokens)))
transcript.AddToTranscript(BatchProofQVector, []byte(fmt.Sprintf("%v", signedTokens)))
prng := transcript.CommitToPRNG("w") prng := transcript.CommitToPRNG("w")
M := new(ristretto.Point).SetZero() M := new(ristretto.Point).SetZero()
@ -58,25 +76,34 @@ func (ts *TokenServer) constructBatchProof(blindedTokens []BlindedToken, signedT
return DiscreteLogEquivalenceProof(ts.k, new(ristretto.Point).SetBase(), ts.Y, M, Z, transcript) return DiscreteLogEquivalenceProof(ts.k, new(ristretto.Point).SetBase(), ts.Y, M, Z, transcript)
} }
// IsValid returns true a SpentToken is valid and has never been spent before, false otherwise. // SpendToken returns true a SpentToken is valid and has never been spent before, false otherwise.
func (ts *TokenServer) IsValid(token SpentToken, data []byte) bool { func (ts *TokenServer) SpendToken(token SpentToken, data []byte) error {
log.Debugf("data: [%s]", data)
ts.mutex.Lock() ts.mutex.Lock()
defer ts.mutex.Unlock() // We only want 1 client at a time redeeming tokens to prevent double-spends 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 { if ts.persistanceService == nil {
return false 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) Ht := sha3.Sum256(token.T)
log.Debugf("token: %x", Ht)
T := new(ristretto.Point).SetElligator(&Ht) T := new(ristretto.Point).SetElligator(&Ht)
W := new(ristretto.Point).ScalarMult(T, ts.k) W := new(ristretto.Point).ScalarMult(T, ts.k)
key := sha3.Sum256(append(token.T, W.Bytes()...)) key := sha3.Sum256(append(token.T, W.Bytes()...))
mac := hmac.New(sha3.New512, key[:]) mac := hmac.New(sha3.New512, key[:])
K := mac.Sum(data) K := mac.Sum(data)
log.Debugf("mac: \n%x\nK:%x\n", token.MAC, K)
result := hmac.Equal(token.MAC, K) result := hmac.Equal(token.MAC, K)
if result == true { if result == true {
ts.seen[hex.EncodeToString(token.T)] = true 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)
} }

View File

@ -19,7 +19,7 @@ type Service interface {
Connect(hostname string, application Application) (bool, error) Connect(hostname string, application Application) (bool, error)
Listen(application Application) error Listen(application Application) error
GetConnection(connectionID string) (Connection, error) GetConnection(connectionID string) (Connection, error)
WaitForCapabilityOrClose(connectionID string, capability string) (Connection, error) WaitForCapabilityOrClose(connectionID string, capability Capability) (Connection, error)
Shutdown() Shutdown()
} }
@ -30,12 +30,13 @@ type Connection interface {
ID() *primitives.Identity ID() *primitives.Identity
Expect() []byte Expect() []byte
SetHostname(hostname string) SetHostname(hostname string)
HasCapability(name string) bool HasCapability(name Capability) bool
SetCapability(name string) SetCapability(name Capability)
SetEncryptionKey(key [32]byte) SetEncryptionKey(key [32]byte)
Send(message []byte) Send(message []byte)
Close() Close()
App() Application App() Application
SetApp(application Application)
IsClosed() bool IsClosed() bool
} }
@ -76,6 +77,11 @@ func (c *connection) App() Application {
return c.app 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 // Hostname returns the hostname of the connection (if the connection has not been authorized it will return the
// temporary hostname identifier) // temporary hostname identifier)
func (c *connection) Hostname() string { func (c *connection) Hostname() string {
@ -100,13 +106,13 @@ func (c *connection) SetHostname(hostname string) {
} }
// SetCapability sets a capability on the connection // 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) log.Debugf("[%v -- %v] Setting Capability %v", c.identity.Hostname(), c.hostname, name)
c.capabilities.Store(name, true) c.capabilities.Store(name, true)
} }
// HasCapability checks if the connection has a given capability // 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) _, ok := c.capabilities.Load(name)
return ok return ok
} }

View File

@ -3,9 +3,12 @@
set -e set -e
pwd pwd
go test ${1} -coverprofile=applications.cover.out -v ./applications 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.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.core.cover.out -v ./primitives/core
go test ${1} -coverprofile=primitives.privacypass.cover.out -v ./primitives/privacypass 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 | \ echo "mode: set" > coverage.out && cat *.cover.out | grep -v mode: | sort -r | \
awk '{if($1 != last) {print $0;last=$1}}' >> coverage.out awk '{if($1 != last) {print $0;last=$1}}' >> coverage.out
rm -rf *.cover.out rm -rf *.cover.out