@@ -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 | |||
@@ -3,3 +3,7 @@ vendor/ | |||
/tor/ | |||
coverage.out | |||
/testing/tor/ | |||
/applications/tor/ | |||
*.db | |||
/applications/tokenboard/tor/ | |||
fuzzing/ |
@@ -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() | |||
} |
@@ -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() | |||
} | |||
} |
@@ -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) | |||
@@ -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") | |||
} | |||
@@ -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 | |||
} |
@@ -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 | |||
} | |||
} |
@@ -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 | |||
} |
@@ -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 | |||
} |
@@ -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) | |||
} | |||
} |
@@ -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() | |||
} |
@@ -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 | |||
} |
@@ -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 |
@@ -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= | |||
@@ -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() { | |||
@@ -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) | |||
} |
@@ -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() | |||
} |
@@ -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() | |||
} |
@@ -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)) | |||
} |
@@ -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") | |||
} | |||
} |
@@ -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 | |||
} |
@@ -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 | |||
} |
@@ -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()) | |||
} |
@@ -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) | |||
} |
@@ -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) | |||
} |
@@ -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" | |||
) |
@@ -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 | |||
} |
@@ -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 | |||
} |
@@ -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()) | |||
} | |||
} |
@@ -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) | |||
} |
@@ -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() | |||
} |
@@ -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 | |||
} |
@@ -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 | |||