forked from cwtch.im/tapir
Move to ristretto255 lib
This commit is contained in:
parent
345d11f506
commit
ff7a32722d
|
@ -4,3 +4,5 @@ vendor/
|
|||
coverage.out
|
||||
/testing/tor/
|
||||
/applications/tor/
|
||||
*.db
|
||||
/applications/tokenboard/tor/
|
||||
|
|
|
@ -4,9 +4,21 @@ import (
|
|||
"cwtch.im/tapir/primitives/core"
|
||||
)
|
||||
|
||||
// Capability defines a status granted to a connection, from an application. That allows the connection to access
|
||||
// other Application or functions within an Application.
|
||||
type Capability string
|
||||
|
||||
// Application defines the interface for all Tapir Applications
|
||||
type Application interface {
|
||||
NewInstance() Application
|
||||
Init(connection Connection)
|
||||
Transcript() *core.Transcript
|
||||
PropagateTranscript(transcript *core.Transcript)
|
||||
}
|
||||
|
||||
// InteractiveApplication defines the interface for interactive Tapir applications (apps that expect the user to send
|
||||
// and receive messages from)
|
||||
type InteractiveApplication interface {
|
||||
Application
|
||||
Listen()
|
||||
}
|
||||
|
|
|
@ -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,7 +17,7 @@ type AuthMessage struct {
|
|||
}
|
||||
|
||||
// AuthCapability defines the Authentication Capability granted by AuthApp
|
||||
const AuthCapability = "AUTH"
|
||||
const AuthCapability = tapir.Capability("AuthenticationCapability")
|
||||
|
||||
// AuthApp is the concrete Application type that handles Authentication
|
||||
type AuthApp struct {
|
||||
|
@ -83,7 +83,9 @@ func (ea *AuthApp) Init(connection tapir.Connection) {
|
|||
|
||||
// Derive a challenge from the transcript of the public parameters of this authentication protocol
|
||||
transcript := ea.Transcript()
|
||||
transcript.AddToTranscript("auth-protocol", []byte(outboundHostname+"-"+inboundHostname))
|
||||
transcript.NewProtocol("auth-app")
|
||||
transcript.AddToTranscript("outbound-hostname", []byte(outboundHostname))
|
||||
transcript.AddToTranscript("inbound-hostname", []byte(inboundHostname))
|
||||
transcript.AddToTranscript("outbound-challenge", outboundAuthMessage)
|
||||
transcript.AddToTranscript("inbound-challenge", inboundAuthMessage)
|
||||
challengeBytes := transcript.CommitToTranscript("3dh-auth-challenge")
|
||||
|
@ -94,14 +96,16 @@ func (ea *AuthApp) Init(connection tapir.Connection) {
|
|||
|
||||
// Since we have set the encryption key on the connection the connection will encrypt any messages we send with that key
|
||||
// To test that the remote peer has done the same we calculate a challenge hash based on the transcript so far and send it to them
|
||||
// along with our hostname
|
||||
// We expect the remote to do the same, and compare the two.
|
||||
// If successful we extend our auth capability to the connection and reassert the hostname.
|
||||
// We note that the only successful scenario here requires that the remote peer have successfully derived the same
|
||||
// encryption key and the same transcript challenge.
|
||||
connection.Send(challengeBytes)
|
||||
connection.Send(append(challengeBytes, []byte(connection.ID().Hostname())...))
|
||||
remoteChallenge := connection.Expect()
|
||||
if subtle.ConstantTimeCompare(challengeBytes, remoteChallenge) == 1 {
|
||||
connection.SetHostname(utils.GetTorV3Hostname(remoteAuthMessage.LongTermPublicKey))
|
||||
assertedHostname := utils.GetTorV3Hostname(remoteAuthMessage.LongTermPublicKey)
|
||||
if subtle.ConstantTimeCompare(append(challengeBytes, []byte(assertedHostname)...), remoteChallenge) == 1 {
|
||||
connection.SetHostname(assertedHostname)
|
||||
connection.SetCapability(AuthCapability)
|
||||
} else {
|
||||
log.Errorf("Failed Decrypt Challenge: [%x] [%x]\n", remoteChallenge, challengeBytes)
|
||||
|
|
|
@ -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,58 @@
|
|||
package applications
|
||||
|
||||
import (
|
||||
"cwtch.im/tapir"
|
||||
"cwtch.im/tapir/primitives/privacypass"
|
||||
"encoding/json"
|
||||
"git.openprivacy.ca/openprivacy/libricochet-go/log"
|
||||
)
|
||||
|
||||
// TokenApplication provides Tokens for PoW
|
||||
type TokenApplication struct {
|
||||
TranscriptApp
|
||||
TokenService *privacypass.TokenServer
|
||||
Tokens []*privacypass.Token
|
||||
}
|
||||
|
||||
// HasTokensCapability is granted once the client has obtained signed tokens
|
||||
const HasTokensCapability = tapir.Capability("HasTokensCapability")
|
||||
|
||||
// NewInstance should always return a new instantiation of the application.
|
||||
func (powapp *TokenApplication) NewInstance() tapir.Application {
|
||||
app := new(TokenApplication)
|
||||
app.TokenService = powapp.TokenService
|
||||
return app
|
||||
}
|
||||
|
||||
// Init is run when the connection is first started.
|
||||
func (powapp *TokenApplication) Init(connection tapir.Connection) {
|
||||
powapp.Transcript().NewProtocol("token-app")
|
||||
log.Debugf(powapp.Transcript().OutputTranscriptToAudit())
|
||||
if connection.IsOutbound() {
|
||||
tokens, blinded := privacypass.GenerateBlindedTokenBatch(10)
|
||||
data, _ := json.Marshal(blinded)
|
||||
connection.Send(data)
|
||||
var signedBatch privacypass.SignedBatchWithProof
|
||||
err := json.Unmarshal(connection.Expect(), &signedBatch)
|
||||
if err == nil {
|
||||
verified := privacypass.UnblindSignedTokenBatch(tokens, blinded, signedBatch.SignedTokens, powapp.TokenService.Y, signedBatch.Proof, powapp.Transcript())
|
||||
if verified {
|
||||
log.Debugf("Successfully obtained signed tokens")
|
||||
powapp.Tokens = tokens
|
||||
connection.SetCapability(HasTokensCapability)
|
||||
return
|
||||
}
|
||||
log.Debugf("Failed to verify signed token batch")
|
||||
}
|
||||
} else {
|
||||
var blinded []privacypass.BlindedToken
|
||||
err := json.Unmarshal(connection.Expect(), &blinded)
|
||||
if err == nil {
|
||||
batchProof := powapp.TokenService.SignBlindedTokenBatch(blinded, powapp.Transcript())
|
||||
log.Debugf(powapp.Transcript().OutputTranscriptToAudit())
|
||||
data, _ := json.Marshal(batchProof)
|
||||
connection.Send(data)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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,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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,151 @@
|
|||
package tokenboard
|
||||
|
||||
import (
|
||||
"cwtch.im/tapir"
|
||||
"cwtch.im/tapir/applications"
|
||||
"cwtch.im/tapir/networks/tor"
|
||||
"cwtch.im/tapir/primitives"
|
||||
"cwtch.im/tapir/primitives/auditable"
|
||||
"cwtch.im/tapir/primitives/privacypass"
|
||||
"errors"
|
||||
"git.openprivacy.ca/openprivacy/libricochet-go/connectivity"
|
||||
"git.openprivacy.ca/openprivacy/libricochet-go/log"
|
||||
"runtime"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
Store *auditable.Store
|
||||
}
|
||||
|
||||
func (h Handler) HandleNewMessages(previousLastCommit []byte) {
|
||||
log.Debugf("Handling Messages After %x", previousLastCommit)
|
||||
messages := h.Store.GetMessagesAfter(previousLastCommit)
|
||||
for _, message := range messages {
|
||||
log.Debugf("Message %s", message)
|
||||
}
|
||||
}
|
||||
|
||||
type FreePaymentHandler struct {
|
||||
tokens []*privacypass.Token
|
||||
TokenService *privacypass.TokenServer
|
||||
ACN connectivity.ACN
|
||||
ServerHostname string
|
||||
}
|
||||
|
||||
func (fph *FreePaymentHandler) MakePayment() {
|
||||
id, sk := primitives.InitializeEphemeralIdentity()
|
||||
var client tapir.Service
|
||||
client = new(tor.BaseOnionService)
|
||||
client.Init(fph.ACN, sk, &id)
|
||||
|
||||
tokenApplication := new(applications.TokenApplication)
|
||||
tokenApplication.TokenService = fph.TokenService
|
||||
powTokenApp := new(applications.ApplicationChain).
|
||||
ChainApplication(new(applications.ProofOfWorkApplication), applications.SuccessfulProofOfWorkCapability).
|
||||
ChainApplication(tokenApplication, applications.HasTokensCapability)
|
||||
client.Connect(fph.ServerHostname, powTokenApp)
|
||||
conn,err := client.WaitForCapabilityOrClose(fph.ServerHostname, applications.HasTokensCapability)
|
||||
if err == nil {
|
||||
powtapp, _ := conn.App().(*applications.TokenApplication)
|
||||
fph.tokens = append(fph.tokens, powtapp.Tokens...)
|
||||
log.Debugf("Transcript: %v", powtapp.Transcript().OutputTranscriptToAudit())
|
||||
conn.Close()
|
||||
}
|
||||
log.Debugf("Error making payment: %v", err)
|
||||
}
|
||||
|
||||
func (fph *FreePaymentHandler) NextToken(data []byte) (privacypass.SpentToken, error) {
|
||||
if len(fph.tokens) == 0 {
|
||||
return privacypass.SpentToken{}, errors.New("No more tokens")
|
||||
}
|
||||
token := fph.tokens[0]
|
||||
fph.tokens = fph.tokens[1:]
|
||||
return token.SpendToken(data), nil
|
||||
}
|
||||
|
||||
func TestTokenBoardApp(t *testing.T) {
|
||||
// numRoutinesStart := runtime.NumGoroutine()
|
||||
log.SetLevel(log.LevelDebug)
|
||||
log.Infof("Number of goroutines open at start: %d", runtime.NumGoroutine())
|
||||
// Connect to Tor
|
||||
var acn connectivity.ACN
|
||||
acn, _ = connectivity.StartTor("./", "")
|
||||
acn.WaitTillBootstrapped()
|
||||
|
||||
// Generate Server Key
|
||||
sid, sk := primitives.InitializeEphemeralIdentity()
|
||||
tokenService := privacypass.NewTokenServer()
|
||||
serverAuditableStore := new(auditable.Store)
|
||||
serverAuditableStore.Init(sid)
|
||||
|
||||
clientAuditableStore := new(auditable.Store)
|
||||
// Only initialize with public parameters
|
||||
sidpubk := sid.PublicKey()
|
||||
publicsid := primitives.InitializeIdentity("server", nil, &sidpubk)
|
||||
clientAuditableStore.Init(publicsid)
|
||||
|
||||
// Init the Server running the Simple App.
|
||||
var service tapir.Service
|
||||
service = new(tor.BaseOnionService)
|
||||
service.Init(acn, sk, &sid)
|
||||
|
||||
// Goroutine Management
|
||||
sg := new(sync.WaitGroup)
|
||||
sg.Add(1)
|
||||
go func() {
|
||||
service.Listen(NewTokenBoardServer(&tokenService, serverAuditableStore))
|
||||
sg.Done()
|
||||
}()
|
||||
|
||||
// Init the Server running the PoW Token App.
|
||||
var powTokenService tapir.Service
|
||||
powTokenService = new(tor.BaseOnionService)
|
||||
spowid, spowk := primitives.InitializeEphemeralIdentity()
|
||||
powTokenService.Init(acn, spowk, &spowid)
|
||||
sg.Add(1)
|
||||
go func() {
|
||||
tokenApplication := new(applications.TokenApplication)
|
||||
tokenApplication.TokenService = &tokenService
|
||||
powTokenApp := new(applications.ApplicationChain).
|
||||
ChainApplication(new(applications.ProofOfWorkApplication), applications.SuccessfulProofOfWorkCapability).
|
||||
ChainApplication(tokenApplication, applications.HasTokensCapability)
|
||||
powTokenService.Listen(powTokenApp)
|
||||
sg.Done()
|
||||
}()
|
||||
|
||||
time.Sleep(time.Second * 60) // wait for server to initialize
|
||||
id, sk := primitives.InitializeEphemeralIdentity()
|
||||
var client tapir.Service
|
||||
client = new(tor.BaseOnionService)
|
||||
client.Init(acn, sk, &id)
|
||||
client.Connect(sid.Hostname(), NewTokenBoardClient(clientAuditableStore, Handler{Store: clientAuditableStore}, &FreePaymentHandler{ACN: acn, TokenService: &tokenService, ServerHostname: spowid.Hostname()}))
|
||||
client.WaitForCapabilityOrClose(sid.Hostname(), applications.AuthCapability)
|
||||
conn, _ := client.GetConnection(sid.Hostname())
|
||||
tba, _ := conn.App().(*Client)
|
||||
tba.PurchaseTokens()
|
||||
tba.Post([]byte("HELLO 1"))
|
||||
tba.Post([]byte("HELLO 2"))
|
||||
tba.Post([]byte("HELLO 3"))
|
||||
tba.Post([]byte("HELLO 4"))
|
||||
tba.Post([]byte("HELLO 5"))
|
||||
tba.Replay()
|
||||
time.Sleep(time.Second * 10) // We have to wait for the async replay request!
|
||||
tba.Post([]byte("HELLO 6"))
|
||||
tba.Post([]byte("HELLO 7"))
|
||||
tba.Post([]byte("HELLO 8"))
|
||||
tba.Post([]byte("HELLO 9"))
|
||||
tba.Post([]byte("HELLO 10"))
|
||||
tba.Replay()
|
||||
time.Sleep(time.Second * 10) // We have to wait for the async replay request!
|
||||
|
||||
if tba.Post([]byte("HELLO 11")) {
|
||||
t.Errorf("Post should have failed.")
|
||||
}
|
||||
|
||||
time.Sleep(time.Second * 10)
|
||||
acn.Close()
|
||||
sg.Wait()
|
||||
}
|
|
@ -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)
|
||||
|
||||
}
|
|
@ -5,7 +5,7 @@ import (
|
|||
"cwtch.im/tapir/primitives/core"
|
||||
)
|
||||
|
||||
// TranscriptApp defines a Tapir Meta=App which provides a global cryptographic transcript
|
||||
// TranscriptApp defines a Tapir Meta-App which provides a global cryptographic transcript
|
||||
type TranscriptApp struct {
|
||||
transcript *core.Transcript
|
||||
}
|
||||
|
@ -25,3 +25,8 @@ func (ta *TranscriptApp) Init(connection tapir.Connection) {
|
|||
func (ta *TranscriptApp) Transcript() *core.Transcript {
|
||||
return ta.transcript
|
||||
}
|
||||
|
||||
// PropagateTranscript overrides the default transcript and propagates a transcript from a previous session
|
||||
func (ta *TranscriptApp) PropagateTranscript(transcript *core.Transcript) {
|
||||
ta.transcript = transcript
|
||||
}
|
||||
|
|
6
go.mod
6
go.mod
|
@ -2,5 +2,11 @@ module cwtch.im/tapir
|
|||
|
||||
require (
|
||||
git.openprivacy.ca/openprivacy/libricochet-go v1.0.4
|
||||
github.com/gtank/merlin v0.1.1-0.20191105220539-8318aed1a79f
|
||||
github.com/gtank/ristretto255 v0.1.1-0.20191011164322-af147e8e15b6
|
||||
go.etcd.io/bbolt v1.3.3
|
||||
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 // indirect
|
||||
)
|
||||
|
||||
go 1.13
|
||||
|
|
13
go.sum
13
go.sum
|
@ -8,11 +8,21 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8
|
|||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/gtank/merlin v0.1.1-0.20191105220539-8318aed1a79f h1:8N8XWLZelZNibkhM1FuF+3Ad3YIbgirjdMiVA0eUkaM=
|
||||
github.com/gtank/merlin v0.1.1-0.20191105220539-8318aed1a79f/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s=
|
||||
github.com/gtank/ristretto255 v0.1.0 h1:WQKpyRsq8Yt7dm0oq6Gj18BGku/Zbj/TOIolBYfmbiI=
|
||||
github.com/gtank/ristretto255 v0.1.0/go.mod h1:Ph5OpO6c7xKUGROZfWVLiJf9icMDwUeIvY4OmlYW69o=
|
||||
github.com/gtank/ristretto255 v0.1.1-0.20191011164322-af147e8e15b6 h1:fYrfnLiiWLCPvmmKbH8AlOwZAtrV0QDKox1HsAEjygY=
|
||||
github.com/gtank/ristretto255 v0.1.1-0.20191011164322-af147e8e15b6/go.mod h1:Ph5OpO6c7xKUGROZfWVLiJf9icMDwUeIvY4OmlYW69o=
|
||||
github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643 h1:hLDRPB66XQT/8+wG9WsDpiCvZf1yKO7sz7scAjSlBa0=
|
||||
github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643/go.mod h1:43+3pMjjKimDBf5Kr4ZFNGbLql1zKkbImw+fZbw3geM=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk=
|
||||
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
golang.org/x/crypto v0.0.0-20190128193316-c7b33c32a30b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f h1:R423Cnkcp5JABoeemiGEPlt9tHXFfw5kvc0yqlxRPWo=
|
||||
|
@ -20,8 +30,9 @@ golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8U
|
|||
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -1,64 +1,113 @@
|
|||
package primitives
|
||||
package auditable
|
||||
|
||||
import (
|
||||
"cwtch.im/tapir/persistence"
|
||||
"cwtch.im/tapir/primitives"
|
||||
"cwtch.im/tapir/primitives/core"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"git.openprivacy.ca/openprivacy/libricochet-go/log"
|
||||
"golang.org/x/crypto/ed25519"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// SignedProof encapsulates a signed proof
|
||||
type SignedProof struct {
|
||||
Commit []byte
|
||||
Proof []byte
|
||||
}
|
||||
type SignedProof []byte
|
||||
|
||||
// Message encapsulates a message for more readable code.
|
||||
type Message []byte
|
||||
|
||||
// State defines an array of messages.
|
||||
type State struct {
|
||||
SignedProof SignedProof
|
||||
Messages []Message
|
||||
}
|
||||
|
||||
// AuditableStore defines a cryptographically secure & auditable transcript of messages sent from multiple
|
||||
//
|
||||
const (
|
||||
auditableDataStoreProtocol = "auditable-data-store"
|
||||
newMessage = "new-message"
|
||||
commit = "commit"
|
||||
collapse = "collapse"
|
||||
)
|
||||
|
||||
// Store defines a cryptographically secure & auditable transcript of messages sent from multiple
|
||||
// unrelated clients to a server.
|
||||
type AuditableStore struct {
|
||||
type Store struct {
|
||||
state State
|
||||
identity Identity
|
||||
identity primitives.Identity
|
||||
transcript *core.Transcript
|
||||
LatestCommit []byte
|
||||
commits map[string]int
|
||||
mutex sync.Mutex
|
||||
db persistence.Service
|
||||
}
|
||||
|
||||
// Init initializes an auditable store
|
||||
func (as *AuditableStore) Init(identity Identity) {
|
||||
func (as *Store) Init(identity primitives.Identity) {
|
||||
as.identity = identity
|
||||
as.transcript = core.NewTranscript("auditable-data-store")
|
||||
as.transcript = core.NewTranscript(auditableDataStoreProtocol)
|
||||
as.commits = make(map[string]int)
|
||||
}
|
||||
|
||||
const messageBucket = "auditable-messages"
|
||||
|
||||
// LoadFromStorage initializes an auditable store from a DB
|
||||
func (as *Store) LoadFromStorage(db persistence.Service) {
|
||||
db.Setup([]string{messageBucket})
|
||||
var messages []Message
|
||||
db.Load(messageBucket, "messages", &messages)
|
||||
log.Debugf("Loaded from Database: %v", len(messages))
|
||||
for _, message := range messages {
|
||||
as.add(message)
|
||||
}
|
||||
log.Debugf("Loaded %v Messages from the Database", len(messages))
|
||||
as.db = db
|
||||
}
|
||||
|
||||
// Add adds a message to the auditable store
|
||||
func (as *AuditableStore) Add(message Message) SignedProof {
|
||||
func (as *Store) Add(message Message) SignedProof {
|
||||
sp := as.add(message)
|
||||
if as.db != nil {
|
||||
as.db.Persist(messageBucket, "messages", as.state.Messages)
|
||||
}
|
||||
return sp
|
||||
}
|
||||
|
||||
// Add adds a message to the auditable store
|
||||
func (as *Store) add(message Message) SignedProof {
|
||||
as.mutex.Lock()
|
||||
defer as.mutex.Unlock()
|
||||
as.transcript.AddToTranscript(newMessage, message)
|
||||
as.LatestCommit = as.transcript.CommitToTranscript(commit)
|
||||
|
||||
as.state.Messages = append(as.state.Messages, message)
|
||||
as.transcript.AddToTranscript("new-message", message)
|
||||
as.LatestCommit = as.identity.Sign(as.transcript.CommitToTranscript("commit"))
|
||||
return SignedProof{as.LatestCommit, as.identity.Sign(as.LatestCommit)}
|
||||
as.state.SignedProof = as.identity.Sign(as.LatestCommit)
|
||||
|
||||
as.commits[base64.StdEncoding.EncodeToString(as.LatestCommit)] = len(as.state.Messages) - 1
|
||||
return as.state.SignedProof
|
||||
}
|
||||
|
||||
// GetState returns the current auditable state
|
||||
func (as *AuditableStore) GetState() (State, SignedProof) {
|
||||
func (as *Store) GetState() State {
|
||||
as.mutex.Lock()
|
||||
defer as.mutex.Unlock()
|
||||
return as.state, SignedProof{as.LatestCommit, as.identity.Sign(as.LatestCommit)}
|
||||
return as.state
|
||||
}
|
||||
|
||||
// GetStateAfter returns the current auditable state after a given commitment
|
||||
func (as *Store) GetStateAfter(commitment []byte) State {
|
||||
if commitment == nil {
|
||||
return as.GetState()
|
||||
}
|
||||
var state State
|
||||
state.Messages = as.GetMessagesAfter(commitment)
|
||||
state.SignedProof = as.identity.Sign(as.LatestCommit)
|
||||
return state
|
||||
}
|
||||
|
||||
// GetMessagesAfter provides access to messages after the given commit.
|
||||
func (as *AuditableStore) GetMessagesAfter(latestCommit []byte) []Message {
|
||||
func (as *Store) GetMessagesAfter(latestCommit []byte) []Message {
|
||||
as.mutex.Lock()
|
||||
defer as.mutex.Unlock()
|
||||
index, ok := as.commits[base64.StdEncoding.EncodeToString(latestCommit)]
|
||||
|
@ -70,27 +119,33 @@ func (as *AuditableStore) GetMessagesAfter(latestCommit []byte) []Message {
|
|||
return as.state.Messages[index+1:]
|
||||
}
|
||||
|
||||
// MergeState merges a given state onto our state, first verifying that the two transcripts align
|
||||
func (as *AuditableStore) MergeState(state State, signedStateProof SignedProof) error {
|
||||
// AppendState merges a given state onto our state, first verifying that the two transcripts align
|
||||
func (as *Store) AppendState(state State) error {
|
||||
next := len(as.state.Messages)
|
||||
for i, m := range state.Messages[next:] {
|
||||
for i, m := range state.Messages {
|
||||
as.state.Messages = append(as.state.Messages, m)
|
||||
|
||||
// We reconstruct the transcript
|
||||
as.transcript.AddToTranscript("new-message", m)
|
||||
as.LatestCommit = as.identity.Sign(as.transcript.CommitToTranscript("commit"))
|
||||
as.transcript.AddToTranscript(newMessage, m)
|
||||
as.LatestCommit = as.transcript.CommitToTranscript(commit)
|
||||
log.Debugf("Adding message %d commit: %x", next+i, as.LatestCommit)
|
||||
as.commits[base64.StdEncoding.EncodeToString(as.LatestCommit)] = next + i
|
||||
}
|
||||
|
||||
// verify that our state matches the servers signed state
|
||||
// this is *not* a security check, as a rogue server can simply sign any state
|
||||
// however committing to a state allows us to build fraud proofs for malicious servers later on.
|
||||
if ed25519.Verify(as.identity.PublicKey(), as.LatestCommit, signedStateProof.Proof) == false {
|
||||
if ed25519.Verify(as.identity.PublicKey(), as.LatestCommit, state.SignedProof) == false {
|
||||
return errors.New("state is not consistent, the server is malicious")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MergeState merges a given state onto our state, first verifying that the two transcripts align
|
||||
func (as *Store) MergeState(state State) error {
|
||||
return as.AppendState(State{Messages: state.Messages[len(as.state.Messages):], SignedProof: state.SignedProof})
|
||||
}
|
||||
|
||||
// VerifyFraudProof - the main idea behind this is as follows:
|
||||
//
|
||||
// Every update requires the server to sign, and thus commit to, a transcript
|
||||
|
@ -101,16 +156,16 @@ func (as *AuditableStore) MergeState(state State, signedStateProof SignedProof)
|
|||
// If, after syncing, the FraudProof still validates, then the server must be malicious.
|
||||
// the information revealed by publicizing a fraud proof is minimal it only reveals the inconsistent transcript commit
|
||||
// and not the cause (which could be reordered messages, dropped messages, additional messages or any combination)
|
||||
func (as *AuditableStore) VerifyFraudProof(signedFraudProof SignedProof, key ed25519.PublicKey) (bool, error) {
|
||||
func (as *Store) VerifyFraudProof(fraudCommit []byte, signedFraudProof SignedProof, key ed25519.PublicKey) (bool, error) {
|
||||
|
||||
if ed25519.Verify(key, signedFraudProof.Commit, signedFraudProof.Proof) == false {
|
||||
if ed25519.Verify(key, fraudCommit, signedFraudProof) == false {
|
||||
// This could happen due to misuse of this function (trying to verify a proof with the wrong public key)
|
||||
// This could happen if the server lies to us and submits a fake state proof, however we cannot use this to
|
||||
// prove that the server is acting maliciously
|
||||
return false, errors.New("signed proof has not been signed by the given public key")
|
||||
}
|
||||
|
||||
_, exists := as.commits[base64.StdEncoding.EncodeToString(signedFraudProof.Commit)]
|
||||
_, exists := as.commits[base64.StdEncoding.EncodeToString(fraudCommit)]
|
||||
if !exists {
|
||||
// We have a message signed by the server which verifies that a message was inserted into the state at a given index
|
||||
// However this directly contradicts our version of the state.
|
||||
|
@ -121,3 +176,10 @@ func (as *AuditableStore) VerifyFraudProof(signedFraudProof SignedProof, key ed2
|
|||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Collapse constructs a verifiable proof stating that the server has collapsed the previous history into the current
|
||||
// root = H(onion)
|
||||
// L = H(Sign(LatestCommit))
|
||||
func (as *Store) Collapse() {
|
||||
as.LatestCommit = as.identity.Sign(as.transcript.CommitToTranscript(collapse))
|
||||
}
|
|
@ -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,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")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,186 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
ristretto "github.com/gtank/ristretto255"
|
||||
)
|
||||
|
||||
// ScalarVector explicit type checking
|
||||
type ScalarVector []*ristretto.Scalar
|
||||
|
||||
// PointVector explicit type checking
|
||||
type PointVector []*ristretto.Element
|
||||
|
||||
// GeneratorVector explicit type checking
|
||||
type GeneratorVector []*ristretto.Element
|
||||
|
||||
// CopyVector safely copies a vector
|
||||
func CopyVector(G GeneratorVector) GeneratorVector {
|
||||
H := make(GeneratorVector, len(G))
|
||||
for i, g := range G {
|
||||
H[i] = new(ristretto.Element).Add(new(ristretto.Element).Zero(), g)
|
||||
}
|
||||
return H
|
||||
}
|
||||
|
||||
// InnerProduct takes the inner product of a and b i.e. <a,b>
|
||||
func InnerProduct(a, b ScalarVector) *ristretto.Scalar {
|
||||
if len(a) != len(b) {
|
||||
panic(fmt.Sprintf("len(a) = %v ; len(b) = %v;", len(a), len(b)))
|
||||
}
|
||||
|
||||
result := new(ristretto.Scalar).Zero()
|
||||
for i, ai := range a {
|
||||
result.Add(result, new(ristretto.Scalar).Multiply(ai, b[i]))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// MultiExp takes in a vector of scalars = {a,b,c...} and a vector of generator = {A,B,C...} and outputs
|
||||
// {aA,bB,cC}
|
||||
func MultiExp(a ScalarVector, G GeneratorVector) *ristretto.Element {
|
||||
if len(a) > len(G) {
|
||||
panic(fmt.Sprintf("len(a) = %v ; len(b) = %v;", len(a), len(G)))
|
||||
}
|
||||
result := new(ristretto.Element).Zero()
|
||||
for i, ai := range a {
|
||||
aG := new(ristretto.Element).ScalarMult(ai, G[i])
|
||||
result = new(ristretto.Element).Add(result, aG)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// SafeAppend is defined for a vector of Scalars
|
||||
func (a ScalarVector) SafeAppend(b *ristretto.Scalar) ScalarVector {
|
||||
list := make(ScalarVector, len(a)+1)
|
||||
for i := 0; i < len(a); i++ {
|
||||
list[i] = a[i]
|
||||
}
|
||||
list[len(a)] = b
|
||||
return list
|
||||
}
|
||||
|
||||
// Join is defined for a vector of Scalars
|
||||
func (a ScalarVector) Join(b ScalarVector) ScalarVector {
|
||||
list := make(ScalarVector, len(a)+len(b))
|
||||
for i := 0; i < len(a); i++ {
|
||||
list[i] = a[i]
|
||||
}
|
||||
for i := len(a); i < len(a)+len(b); i++ {
|
||||
list[i] = b[i-len(a)]
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
// SafeAppend as defined for a vector of Generators
|
||||
func (a GeneratorVector) SafeAppend(b *ristretto.Element) GeneratorVector {
|
||||
list := make(GeneratorVector, len(a)+1)
|
||||
for i := 0; i < len(a); i++ {
|
||||
list[i] = a[i]
|
||||
}
|
||||
list[len(a)] = b
|
||||
return list
|
||||
}
|
||||
|
||||
// Join as defined for a vector of Generators
|
||||
func (a GeneratorVector) Join(b GeneratorVector) GeneratorVector {
|
||||
list := make(GeneratorVector, len(a)+len(b))
|
||||
for i := 0; i < len(a); i++ {
|
||||
list[i] = a[i]
|
||||
}
|
||||
for i := len(a); i < len(a)+len(b); i++ {
|
||||
list[i] = b[i-len(a)]
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
// VectorAddScalar takes in a vector v = {a,b,c..} and a scalar s and outputs {a+s,b+s,c+s....}
|
||||
func VectorAddScalar(vector ScalarVector, scalar *ristretto.Scalar) ScalarVector {
|
||||
result := make(ScalarVector, len(vector))
|
||||
for i := range vector {
|
||||
result[i] = new(ristretto.Scalar)
|
||||
result[i].Add(vector[i], scalar)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// VectorNegate takes in a vector v = {a,b,c..} and a scalar s and outputs {-a,-b,-c}
|
||||
func VectorNegate(vector ScalarVector) ScalarVector {
|
||||
result := make(ScalarVector, len(vector))
|
||||
for i := range vector {
|
||||
result[i] = new(ristretto.Scalar).Negate(vector[i])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// VectorMulScalar takes in a vector v = {a,b,c..} and a scalar s and outputs {as,bs,cs....}
|
||||
func VectorMulScalar(vector ScalarVector, scalar *ristretto.Scalar) ScalarVector {
|
||||
result := make(ScalarVector, len(vector))
|
||||
for i := range vector {
|
||||
result[i] = new(ristretto.Scalar)
|
||||
result[i].Multiply(vector[i], scalar)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// EntrywiseSum takes the entry wise sum of two vectors
|
||||
func EntrywiseSum(vector ScalarVector, vector2 ScalarVector) ScalarVector {
|
||||
result := make(ScalarVector, len(vector))
|
||||
for i, v := range vector {
|
||||
result[i] = new(ristretto.Scalar)
|
||||
result[i].Add(v, vector2[i])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// EntrywiseSub takes the entry wise subtraction of two vectors
|
||||
func EntrywiseSub(vector ScalarVector, vector2 ScalarVector) ScalarVector {
|
||||
result := make(ScalarVector, len(vector))
|
||||
for i, v := range vector {
|
||||
result[i] = new(ristretto.Scalar)
|
||||
result[i].Subtract(v, vector2[i])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// EntryWiseProduct takes the entry wise product of two vectors
|
||||
func EntryWiseProduct(vector ScalarVector, vector2 ScalarVector) ScalarVector {
|
||||
result := make(ScalarVector, len(vector))
|
||||
for i, v := range vector {
|
||||
result[i] = new(ristretto.Scalar)
|
||||
result[i].Multiply(v, vector2[i])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// One returns a ristretto scalar == 1
|
||||
func One() *ristretto.Scalar {
|
||||
one := new(ristretto.Scalar)
|
||||
one.Decode([]byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})
|
||||
return one
|
||||
}
|
||||
|
||||
// IdentityVector is a convenience function to generate a vector v = {1,1,1...1}
|
||||
func IdentityVector(n int) ScalarVector {
|
||||
result := make(ScalarVector, n)
|
||||
one := new(ristretto.Scalar)
|
||||
one.Decode([]byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})
|
||||
for i := 0; i < n; i++ {
|
||||
result[i] = one
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// PowerVector creates a vector v = {1,x,x^2,x^3..x^n}
|
||||
func PowerVector(x *ristretto.Scalar, n int) ScalarVector {
|
||||
result := make(ScalarVector, n)
|
||||
one := new(ristretto.Scalar)
|
||||
one.Decode([]byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})
|
||||
result[0] = one
|
||||
result[1] = x
|
||||
for i := 2; i < n; i++ {
|
||||
result[i] = new(ristretto.Scalar)
|
||||
result[i].Multiply(result[i-1], x)
|
||||
}
|
||||
return result
|
||||
}
|
|
@ -2,32 +2,28 @@ package core
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/bwesterb/go-ristretto"
|
||||
"github.com/gtank/merlin"
|
||||
ristretto "github.com/gtank/ristretto255"
|
||||
"golang.org/x/crypto/sha3"
|
||||
"hash"
|
||||
"io"
|
||||
)
|
||||
|
||||
// Transcript implements a transcript of a public coin argument.
|
||||
// Transcript provides a consistent transcript primitive for our protocols
|
||||
//
|
||||
// We have the following goals:
|
||||
// - Provide a consisted transcript API for our zero knowledge protocols
|
||||
// - Allow sequential proofs over a common transcript (ensuring a single proof cannot be extracted standalone)
|
||||
// - produce an auditable human-readable transcript.
|
||||
// - be able to produce a human-readable transcript for auditing.
|
||||
//
|
||||
// The design of this API was inspired by Merlin: https://docs.rs/crate/merlin/
|
||||
//
|
||||
// At some point we might want to extend this to be compatible with Merlin transcripts, built on STROBE
|
||||
type Transcript struct {
|
||||
hash hash.Hash
|
||||
merlinTranscript *merlin.Transcript
|
||||
transcript string
|
||||
}
|
||||
|
||||
// NewTranscript creates a new Transcript with the given Label, the label should be unique to the application
|
||||
func NewTranscript(label string) *Transcript {
|
||||
transcript := new(Transcript)
|
||||
transcript.hash = sha3.New256()
|
||||
transcript.AddToTranscript("protocol", []byte(label))
|
||||
transcript.merlinTranscript = merlin.NewTranscript(label)
|
||||
return transcript
|
||||
}
|
||||
|
||||
|
@ -36,7 +32,13 @@ func NewTranscript(label string) *Transcript {
|
|||
func (t *Transcript) AddToTranscript(label string, b []byte) {
|
||||
op := fmt.Sprintf("%s (%d) %x;", label, len(b), b)
|
||||
t.transcript = fmt.Sprintf("%v\n%v", t.transcript, op)
|
||||
t.hash.Write([]byte(op))
|
||||
t.merlinTranscript.AppendMessage([]byte(label), b)
|
||||
}
|
||||
|
||||
// AddElementToTranscript appends a value to the transcript with the given label
|
||||
// This binds the given data to the label.
|
||||
func (t *Transcript) AddElementToTranscript(label string, element *ristretto.Element) {
|
||||
t.AddToTranscript(label, element.Encode([]byte{}))
|
||||
}
|
||||
|
||||
// OutputTranscriptToAudit outputs a human-readable copy of the transcript so far.
|
||||
|
@ -44,11 +46,18 @@ func (t Transcript) OutputTranscriptToAudit() string {
|
|||
return t.transcript
|
||||
}
|
||||
|
||||
// NewProtocol provides explicit protocol separation in a transcript (more readable audit scripts and even more explicit
|
||||
// binding of committed values to a given context)
|
||||
func (t *Transcript) NewProtocol(label string) {
|
||||
op := fmt.Sprintf("---- new-protcol: %s ----", label)
|
||||
t.transcript = fmt.Sprintf("%v\n%v", t.transcript, op)
|
||||
t.merlinTranscript.AppendMessage([]byte("protocol"), []byte(label))
|
||||
}
|
||||
|
||||
// CommitToTranscript generates a challenge based on the current transcript, it also commits the challenge to the transcript.
|
||||
func (t *Transcript) CommitToTranscript(label string) []byte {
|
||||
t.AddToTranscript("commit", []byte(label))
|
||||
b := t.hash.Sum([]byte{})
|
||||
t.AddToTranscript(label, b)
|
||||
b := t.merlinTranscript.ExtractBytes([]byte(label), 64)
|
||||
t.transcript = fmt.Sprintf("%v\nextract %v: %v", t.transcript, label, b)
|
||||
return b
|
||||
}
|
||||
|
||||
|
@ -59,25 +68,39 @@ type PRNG struct {
|
|||
|
||||
// Next returns the next "random" scalar from the PRNG
|
||||
func (prng *PRNG) Next() *ristretto.Scalar {
|
||||
buf := [32]byte{}
|
||||
buf := [64]byte{}
|
||||
io.ReadFull(prng.prng, buf[:])
|
||||
return new(ristretto.Scalar).SetBytes(&buf)
|
||||
next := new(ristretto.Scalar)
|
||||
next.FromUniformBytes(buf[:])
|
||||
return next
|
||||
}
|
||||
|
||||
// CommitToPRNG commits the label to the transcript and derives a PRNG from the transcript.
|
||||
func (t *Transcript) CommitToPRNG(label string) PRNG {
|
||||
t.AddToTranscript("commit-prng", []byte(label))
|
||||
b := t.hash.Sum([]byte{})
|
||||
t.AddToTranscript(label, b)
|
||||
b := t.merlinTranscript.ExtractBytes([]byte(label), 64)
|
||||
prng := sha3.NewShake256()
|
||||
prng.Write(b)
|
||||
return PRNG{prng: prng}
|
||||
}
|
||||
|
||||
// CommitToGenerator derives a verifiably random generator from the transcript
|
||||
func (t *Transcript) CommitToGenerator(label string) *ristretto.Element {
|
||||
c := t.CommitToTranscript(label)
|
||||
return new(ristretto.Element).FromUniformBytes(c)
|
||||
}
|
||||
|
||||
// CommitToGenerators derives a set of verifiably random generators from the transcript
|
||||
func (t *Transcript) CommitToGenerators(label string, n int) (generators []*ristretto.Element) {
|
||||
for i := 0; i < n; i++ {
|
||||
generators = append(generators, t.CommitToGenerator(fmt.Sprintf("%v-%d", label, i)))
|
||||
}
|
||||
return generators
|
||||
}
|
||||
|
||||
// CommitToTranscriptScalar is a convenience method for CommitToTranscript which returns a ristretto Scalar
|
||||
func (t *Transcript) CommitToTranscriptScalar(label string) *ristretto.Scalar {
|
||||
c := t.CommitToTranscript(label)
|
||||
cs := [32]byte{}
|
||||
copy(cs[:], c[:])
|
||||
return new(ristretto.Scalar).SetBytes(&cs)
|
||||
s := new(ristretto.Scalar)
|
||||
s.FromUniformBytes(c[:])
|
||||
return s
|
||||
}
|
||||
|
|
|
@ -1,71 +0,0 @@
|
|||
package primitives
|
||||
|
||||
import (
|
||||
"cwtch.im/tapir/primitives/core"
|
||||
"github.com/bwesterb/go-ristretto"
|
||||
)
|
||||
|
||||
// DLProof Encapsulates a Discrete Log / Schnorr Proof
|
||||
// Note that these parameters are read-only.
|
||||
type DLProof struct {
|
||||
V, A ristretto.Point
|
||||
R ristretto.Scalar
|
||||
}
|
||||
|
||||
// DiscreteLogProof - Proof of Knowledge of Exponent
|
||||
// Given V = xG
|
||||
// Peggy: z := choose randomly from Zq
|
||||
// A := zG
|
||||
// c := H(transcript(G,V,A)) mod q
|
||||
// r := (z + cx) mod q
|
||||
//
|
||||
// Sends A,r,V to Vicky
|
||||
func DiscreteLogProof(x ristretto.Scalar, v ristretto.Point, transcript *core.Transcript) (proof DLProof) {
|
||||
|
||||
transcript.AddToTranscript("G", new(ristretto.Point).SetBase().Bytes())
|
||||
|
||||
// We bind the proof to our public V
|
||||
proof.V = v
|
||||
transcript.AddToTranscript("V", proof.V.Bytes())
|
||||
|
||||
// Generate a random z
|
||||
// A := zG
|
||||
z := new(ristretto.Scalar).Rand()
|
||||
proof.A = *new(ristretto.Point).ScalarMultBase(z)
|
||||
transcript.AddToTranscript("A", proof.A.Bytes())
|
||||
|
||||
// Derive Challenge
|
||||
c := transcript.CommitToTranscriptScalar("c")
|
||||
|
||||
// r := (z + cx) mod p
|
||||
cx := new(ristretto.Scalar).Mul(c, &x)
|
||||
proof.R = *new(ristretto.Scalar).Add(z, cx)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// VerifyDiscreteLogProof validates a given Schnorr Proof
|
||||
// Vicky gets A,r,V from Peggy
|
||||
// Vicky computes c := H(transcript(G,V,A)) mod q
|
||||
// Vicky checks rG := A + cV
|
||||
// rG ?= zG + cV
|
||||
// (z+cx)G ?= zG + cV
|
||||
// ?= zG + cxG
|
||||
// Thus demonstrating that Peggy knows the discrete log to V
|
||||
func VerifyDiscreteLogProof(proof DLProof, transcript *core.Transcript) bool {
|
||||
|
||||
transcript.AddToTranscript("G", new(ristretto.Point).SetBase().Bytes())
|
||||
transcript.AddToTranscript("V", proof.V.Bytes())
|
||||
transcript.AddToTranscript("A", proof.A.Bytes())
|
||||
c := transcript.CommitToTranscriptScalar("c")
|
||||
|
||||
// Compute left hand side
|
||||
lhs := new(ristretto.Point).ScalarMultBase(&proof.R)
|
||||
|
||||
// Compute right hand side
|
||||
cV := new(ristretto.Point).ScalarMult(&proof.V, c)
|
||||
rhs := new(ristretto.Point).Add(&proof.A, cV)
|
||||
|
||||
// Result of verification: lhs ?= rhs
|
||||
return lhs.Equals(rhs)
|
||||
}
|
|
@ -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"
|
||||
)
|
|
@ -1,12 +1,13 @@
|
|||
package privacypass
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"cwtch.im/tapir/primitives/core"
|
||||
"github.com/bwesterb/go-ristretto"
|
||||
ristretto "github.com/gtank/ristretto255"
|
||||
)
|
||||
|
||||
// DLEQProof encapsulates a Chaum-Pedersen DLEQ Proof
|
||||
// David Chaum and Torben P. Pedersen. Wallet databaseswith observers. In Ernest F. Brickell, editor,CRYPTO’92,volume 740 ofLNCS, pages 89–105. Springer, Heidelberg,August 1993
|
||||
//gut In Ernest F. Brickell, editor,CRYPTO’92,volume 740 ofLNCS, pages 89–105. Springer, Heidelberg,August 1993
|
||||
type DLEQProof struct {
|
||||
C *ristretto.Scalar
|
||||
S *ristretto.Scalar
|
||||
|
@ -21,20 +22,23 @@ type DLEQProof struct {
|
|||
// s := (t + ck) mod q
|
||||
//
|
||||
// Sends c,s to Vicky
|
||||
func DiscreteLogEquivalenceProof(k *ristretto.Scalar, X *ristretto.Point, Y *ristretto.Point, P *ristretto.Point, Q *ristretto.Point, transcript *core.Transcript) DLEQProof {
|
||||
t := new(ristretto.Scalar).Rand()
|
||||
A := new(ristretto.Point).ScalarMult(X, t)
|
||||
B := new(ristretto.Point).ScalarMult(P, t)
|
||||
func DiscreteLogEquivalenceProof(k *ristretto.Scalar, X *ristretto.Element, Y *ristretto.Element, P *ristretto.Element, Q *ristretto.Element, transcript *core.Transcript) DLEQProof {
|
||||
private := make([]byte, 64)
|
||||
rand.Read(private)
|
||||
t := new(ristretto.Scalar)
|
||||
t.FromUniformBytes(private)
|
||||
A := new(ristretto.Element).ScalarMult(t, X)
|
||||
B := new(ristretto.Element).ScalarMult(t, P)
|
||||
|
||||
transcript.AddToTranscript("X", X.Bytes())
|
||||
transcript.AddToTranscript("Y", Y.Bytes())
|
||||
transcript.AddToTranscript("P", P.Bytes())
|
||||
transcript.AddToTranscript("Q", Q.Bytes())
|
||||
transcript.AddToTranscript("A", A.Bytes())
|
||||
transcript.AddToTranscript("B", B.Bytes())
|
||||
transcript.AddToTranscript(DLEQX, X.Encode(nil))
|
||||
transcript.AddToTranscript(DLEQY, Y.Encode(nil))
|
||||
transcript.AddToTranscript(DLEQP, P.Encode(nil))
|
||||
transcript.AddToTranscript(DLEQQ, Q.Encode(nil))
|
||||
transcript.AddToTranscript(DLEQA, A.Encode(nil))
|
||||
transcript.AddToTranscript(DLEQB, B.Encode(nil))
|
||||
|
||||
c := transcript.CommitToTranscriptScalar("c")
|
||||
s := new(ristretto.Scalar).Sub(t, new(ristretto.Scalar).Mul(c, k))
|
||||
s := new(ristretto.Scalar).Subtract(t, new(ristretto.Scalar).Multiply(c, k))
|
||||
return DLEQProof{c, s}
|
||||
}
|
||||
|
||||
|
@ -48,22 +52,22 @@ func DiscreteLogEquivalenceProof(k *ristretto.Scalar, X *ristretto.Point, Y *ris
|
|||
// B' = P'+Q' == sP + cQ ?= sP + ckP == (s+ck)P == tP == B
|
||||
// c' := H(transcript(X,Y,P,Q,A',B'))
|
||||
// Tests c ?= c
|
||||
func VerifyDiscreteLogEquivalenceProof(dleq DLEQProof, X *ristretto.Point, Y *ristretto.Point, P *ristretto.Point, Q *ristretto.Point, transcript *core.Transcript) bool {
|
||||
func VerifyDiscreteLogEquivalenceProof(dleq DLEQProof, X *ristretto.Element, Y *ristretto.Element, P *ristretto.Element, Q *ristretto.Element, transcript *core.Transcript) bool {
|
||||
|
||||
Xs := new(ristretto.Point).ScalarMult(X, dleq.S)
|
||||
Yc := new(ristretto.Point).ScalarMult(Y, dleq.C)
|
||||
Ps := new(ristretto.Point).ScalarMult(P, dleq.S)
|
||||
Qc := new(ristretto.Point).ScalarMult(Q, dleq.C)
|
||||
Xs := new(ristretto.Element).ScalarMult(dleq.S, X)
|
||||
Yc := new(ristretto.Element).ScalarMult(dleq.C, Y)
|
||||
Ps := new(ristretto.Element).ScalarMult(dleq.S, P)
|
||||
Qc := new(ristretto.Element).ScalarMult(dleq.C, Q)
|
||||
|
||||
A := new(ristretto.Point).Add(Xs, Yc)
|
||||
B := new(ristretto.Point).Add(Ps, Qc)
|
||||
A := new(ristretto.Element).Add(Xs, Yc)
|
||||
B := new(ristretto.Element).Add(Ps, Qc)
|
||||
|
||||
transcript.AddToTranscript("X", X.Bytes())
|
||||
transcript.AddToTranscript("Y", Y.Bytes())
|
||||
transcript.AddToTranscript("P", P.Bytes())
|
||||
transcript.AddToTranscript("Q", Q.Bytes())
|
||||
transcript.AddToTranscript("A", A.Bytes())
|
||||
transcript.AddToTranscript("B", B.Bytes())
|
||||
transcript.AddToTranscript(DLEQX, X.Encode(nil))
|
||||
transcript.AddToTranscript(DLEQY, Y.Encode(nil))
|
||||
transcript.AddToTranscript(DLEQP, P.Encode(nil))
|
||||
transcript.AddToTranscript(DLEQQ, Q.Encode(nil))
|
||||
transcript.AddToTranscript(DLEQA, A.Encode(nil))
|
||||
transcript.AddToTranscript(DLEQB, B.Encode(nil))
|
||||
|
||||
return transcript.CommitToTranscriptScalar("c").Equals(dleq.C)
|
||||
return transcript.CommitToTranscriptScalar("c").Equal(dleq.C) == 1
|
||||
}
|
||||
|
|
|
@ -6,7 +6,8 @@ import (
|
|||
"cwtch.im/tapir/primitives/core"
|
||||
"fmt"
|
||||
"git.openprivacy.ca/openprivacy/libricochet-go/log"
|
||||
"github.com/bwesterb/go-ristretto"
|
||||
ristretto "github.com/gtank/ristretto255"
|
||||
|
||||
"golang.org/x/crypto/sha3"
|
||||
)
|
||||
|
||||
|
@ -15,17 +16,17 @@ import (
|
|||
type Token struct {
|
||||
t []byte
|
||||
r *ristretto.Scalar
|
||||
W *ristretto.Point
|
||||
W *ristretto.Element
|
||||
}
|
||||
|
||||
// BlindedToken encapsulates a Blinded Token
|
||||
type BlindedToken struct {
|
||||
P *ristretto.Point
|
||||
P *ristretto.Element
|
||||
}
|
||||
|
||||
// SignedToken encapsulates a Signed (Blinded) Token
|
||||
type SignedToken struct {
|
||||
Q *ristretto.Point
|
||||
Q *ristretto.Element
|
||||
}
|
||||
|
||||
// SpentToken encapsulates the parameters needed to spend a Token
|
||||
|
@ -36,7 +37,7 @@ type SpentToken struct {
|
|||
|
||||
// TokenPaymentHandler defines an interface with external payment processors
|
||||
type TokenPaymentHandler interface {
|
||||
MakePayment(int)
|
||||
MakePayment()
|
||||
NextToken(data []byte) (SpentToken, error)
|
||||
}
|
||||
|
||||
|
@ -45,23 +46,26 @@ type TokenPaymentHandler interface {
|
|||
func (t *Token) GenBlindedToken() BlindedToken {
|
||||
t.t = make([]byte, 32)
|
||||
rand.Read(t.t)
|
||||
t.r = new(ristretto.Scalar).Rand()
|
||||
t.r = new(ristretto.Scalar)
|
||||
b := make([]byte, 64)
|
||||
rand.Read(b)
|
||||
t.r.FromUniformBytes(b)
|
||||
|
||||
Ht := sha3.Sum256(t.t)
|
||||
Ht := sha3.Sum512(t.t)
|
||||
log.Debugf("token: %x", Ht)
|
||||
T := new(ristretto.Point).SetElligator(&Ht)
|
||||
P := new(ristretto.Point).ScalarMult(T, t.r)
|
||||
T := new(ristretto.Element).FromUniformBytes(Ht[:])
|
||||
P := new(ristretto.Element).ScalarMult(t.r, T)
|
||||
return BlindedToken{P}
|
||||
}
|
||||
|
||||
// unblindSignedToken unblinds a token that has been signed by a server
|
||||
func (t *Token) unblindSignedToken(token SignedToken) {
|
||||
t.W = new(ristretto.Point).ScalarMult(token.Q, new(ristretto.Scalar).Inverse(t.r))
|
||||
t.W = new(ristretto.Element).ScalarMult(new(ristretto.Scalar).Invert(t.r), token.Q)
|
||||
}
|
||||
|
||||
// SpendToken binds the token with data and then redeems the token
|
||||
func (t *Token) SpendToken(data []byte) SpentToken {
|
||||
key := sha3.Sum256(append(t.t, t.W.Bytes()...))
|
||||
key := sha3.Sum256(append(t.t, t.W.Encode(nil)...))
|
||||
mac := hmac.New(sha3.New512, key[:])
|
||||
return SpentToken{t.t, mac.Sum(data)}
|
||||
}
|
||||
|
@ -76,27 +80,29 @@ func GenerateBlindedTokenBatch(num int) (tokens []*Token, blindedTokens []Blinde
|
|||
}
|
||||
|
||||
// verifyBatchProof verifies a given batch proof (see also UnblindSignedTokenBatch)
|
||||
func verifyBatchProof(dleq DLEQProof, Y *ristretto.Point, blindedTokens []BlindedToken, signedTokens []SignedToken, transcript *core.Transcript) bool {
|
||||
transcript.AddToTranscript("X", new(ristretto.Point).SetBase().Bytes())
|
||||
transcript.AddToTranscript("Y", Y.Bytes())
|
||||
transcript.AddToTranscript("P[]", []byte(fmt.Sprintf("%v", blindedTokens)))
|
||||
transcript.AddToTranscript("Q[]", []byte(fmt.Sprintf("%v", signedTokens)))
|
||||
func verifyBatchProof(dleq DLEQProof, Y *ristretto.Element, blindedTokens []BlindedToken, signedTokens []SignedToken, transcript *core.Transcript) bool {
|
||||
transcript.NewProtocol(BatchProofProtocol)
|
||||
transcript.AddToTranscript(BatchProofX, new(ristretto.Element).Base().Encode(nil))
|
||||
transcript.AddToTranscript(BatchProofY, Y.Encode(nil))
|
||||
transcript.AddToTranscript(BatchProofPVector, []byte(fmt.Sprintf("%v", blindedTokens)))
|
||||
transcript.AddToTranscript(BatchProofQVector, []byte(fmt.Sprintf("%v", signedTokens)))
|
||||
prng := transcript.CommitToPRNG("w")
|
||||
M := new(ristretto.Point).SetZero()
|
||||
Z := new(ristretto.Point).SetZero()
|
||||
M := new(ristretto.Element).Zero()
|
||||
Z := new(ristretto.Element).Zero()
|
||||
for i := range blindedTokens {
|
||||
c := prng.Next()
|
||||
M = new(ristretto.Point).Add(new(ristretto.Point).ScalarMult(blindedTokens[i].P, c), M)
|
||||
Z = new(ristretto.Point).Add(new(ristretto.Point).ScalarMult(signedTokens[i].Q, c), Z)
|
||||
M = new(ristretto.Element).Add(new(ristretto.Element).ScalarMult(c, blindedTokens[i].P), M)
|
||||
Z = new(ristretto.Element).Add(new(ristretto.Element).ScalarMult(c, signedTokens[i].Q), Z)
|
||||
}
|
||||
return VerifyDiscreteLogEquivalenceProof(dleq, new(ristretto.Point).SetBase(), Y, M, Z, transcript)
|
||||
return VerifyDiscreteLogEquivalenceProof(dleq, new(ristretto.Element).Base(), Y, M, Z, transcript)
|
||||
}
|
||||
|
||||
// UnblindSignedTokenBatch taking in a set of tokens, their blinded & signed counterparts, a server public key (Y), a DLEQ proof and a transcript
|
||||
// verifies that the signing procedure has taken place correctly and unblinds the tokens.
|
||||
func UnblindSignedTokenBatch(tokens []*Token, blindedTokens []BlindedToken, signedTokens []SignedToken, Y *ristretto.Point, proof DLEQProof, transcript *core.Transcript) bool {
|
||||
func UnblindSignedTokenBatch(tokens []*Token, blindedTokens []BlindedToken, signedTokens []SignedToken, Y *ristretto.Element, proof DLEQProof, transcript *core.Transcript) bool {
|
||||
verified := verifyBatchProof(proof, Y, blindedTokens, signedTokens, transcript)
|
||||
if !verified {
|
||||
log.Debugf(transcript.OutputTranscriptToAudit())
|
||||
return false
|
||||
}
|
||||
for i, t := range tokens {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package privacypass
|
||||
|
||||
import (
|
||||
"cwtch.im/tapir/persistence"
|
||||
"cwtch.im/tapir/primitives/core"
|
||||
"git.openprivacy.ca/openprivacy/libricochet-go/log"
|
||||
"testing"
|
||||
|
@ -17,30 +18,32 @@ func TestToken_SpendToken(t *testing.T) {
|
|||
|
||||
spentToken := token.SpendToken([]byte("Hello"))
|
||||
|
||||
if server.IsValid(spentToken, []byte("Hello World")) == true {
|
||||
if server.SpendToken(spentToken, []byte("Hello World")) == nil {
|
||||
t.Errorf("Token Should be InValid")
|
||||
}
|
||||
|
||||
if server.IsValid(spentToken, []byte("Hello")) == false {
|
||||
t.Errorf("Token Should be Valid")
|
||||
if err := server.SpendToken(spentToken, []byte("Hello")); err != nil {
|
||||
t.Errorf("Token Should be Valid: %v", err)
|
||||
}
|
||||
|
||||
if server.IsValid(spentToken, []byte("Hello")) == true {
|
||||
if err := server.SpendToken(spentToken, []byte("Hello")); err == nil {
|
||||
t.Errorf("Token Should be Spent")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateBlindedTokenBatch(t *testing.T) {
|
||||
log.SetLevel(log.LevelDebug)
|
||||
db := new(persistence.BoltPersistence)
|
||||
db.Open("tokens.db")
|
||||
server := NewTokenServer()
|
||||
|
||||
clientTranscript := core.NewTranscript("privacyPass")
|
||||
serverTranscript := core.NewTranscript("privacyPass")
|
||||
|
||||
tokens, blindedTokens := GenerateBlindedTokenBatch(10)
|
||||
signedTokens, proof := server.SignBlindedTokenBatch(blindedTokens, serverTranscript)
|
||||
batchProof := server.SignBlindedTokenBatch(blindedTokens, serverTranscript)
|
||||
|
||||
verified := UnblindSignedTokenBatch(tokens, blindedTokens, signedTokens, server.Y, proof, clientTranscript)
|
||||
verified := UnblindSignedTokenBatch(tokens, blindedTokens, batchProof.SignedTokens, server.Y, batchProof.Proof, clientTranscript)
|
||||
|
||||
if !verified {
|
||||
t.Errorf("Something went wrong, the proof did not pass")
|
||||
|
@ -49,8 +52,8 @@ func TestGenerateBlindedTokenBatch(t *testing.T) {
|
|||
// Attempt to Spend All the tokens
|
||||
for _, token := range tokens {
|
||||
spentToken := token.SpendToken([]byte("Hello"))
|
||||
if server.IsValid(spentToken, []byte("Hello")) == false {
|
||||
t.Errorf("Token Should be Valid")
|
||||
if err := server.SpendToken(spentToken, []byte("Hello")); err != nil {
|
||||
t.Errorf("Token Should be Valid: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -58,8 +61,9 @@ func TestGenerateBlindedTokenBatch(t *testing.T) {
|
|||
t.Logf("Server Transcript,: %s", serverTranscript.OutputTranscriptToAudit())
|
||||
|
||||
wrongTranscript := core.NewTranscript("wrongTranscript")
|
||||
verified = UnblindSignedTokenBatch(tokens, blindedTokens, signedTokens, server.Y, proof, wrongTranscript)
|
||||
verified = UnblindSignedTokenBatch(tokens, blindedTokens, batchProof.SignedTokens, server.Y, batchProof.Proof, wrongTranscript)
|
||||
if verified {
|
||||
t.Errorf("Something went wrong, the proof passed with wrong transcript: %s", wrongTranscript.OutputTranscriptToAudit())
|
||||
}
|
||||
db.Close()
|
||||
}
|
||||
|
|
|
@ -2,11 +2,12 @@ package privacypass
|
|||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"cwtch.im/tapir/persistence"
|
||||
"cwtch.im/tapir/primitives/core"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"git.openprivacy.ca/openprivacy/libricochet-go/log"
|
||||
"github.com/bwesterb/go-ristretto"
|
||||
ristretto "github.com/gtank/ristretto255"
|
||||
"golang.org/x/crypto/sha3"
|
||||
"sync"
|
||||
)
|
||||
|
@ -14,69 +15,102 @@ import (
|
|||
// TokenServer implements a token server.
|
||||
type TokenServer struct {
|
||||
k *ristretto.Scalar
|
||||
Y *ristretto.Point
|
||||
Y *ristretto.Element
|
||||
seen map[string]bool
|
||||
persistanceService persistence.Service
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
// SignedBatchWithProof encapsulates a signed batch of blinded tokens with a batch proof for verification
|
||||
type SignedBatchWithProof struct {
|
||||
SignedTokens []SignedToken
|
||||
Proof DLEQProof
|
||||
}
|
||||
|
||||
const tokenBucket = "tokens"
|
||||
|
||||
// NewTokenServer generates a new TokenServer (used mostly for testing with ephemeral instances)
|
||||
func NewTokenServer() TokenServer {
|
||||
k := new(ristretto.Scalar).Rand()
|
||||
return TokenServer{k, new(ristretto.Point).ScalarMultBase(k), make(map[string]bool), sync.Mutex{}}
|
||||
k := new(ristretto.Scalar)
|
||||
b := make([]byte, 64)
|
||||
rand.Read(b)
|
||||
k.FromUniformBytes(b)
|
||||
return TokenServer{k, new(ristretto.Element).ScalarBaseMult(k), make(map[string]bool), nil, sync.Mutex{}}
|
||||
}
|
||||
|
||||
// NewTokenServerFromStore generates a new TokenServer backed by a persistence service.
|
||||
func NewTokenServerFromStore(persistenceService persistence.Service) TokenServer {
|
||||
k := new(ristretto.Scalar)
|
||||
b := make([]byte, 64)
|
||||
rand.Read(b)
|
||||
k.FromUniformBytes(b)
|
||||
persistenceService.Setup([]string{tokenBucket})
|
||||
return TokenServer{k, new(ristretto.Element).ScalarBaseMult(k), make(map[string]bool), persistenceService, sync.Mutex{}}
|
||||
}
|
||||
|
||||
// SignBlindedToken calculates kP for the given BlindedToken P
|
||||
func (ts *TokenServer) SignBlindedToken(bt BlindedToken) SignedToken {
|
||||
Q := new(ristretto.Point).ScalarMult(bt.P, ts.k)
|
||||
Q := new(ristretto.Element).ScalarMult(ts.k, bt.P)
|
||||
return SignedToken{Q}
|
||||
}
|
||||
|
||||
// SignBlindedTokenBatch signs a batch of blinded tokens under a given transcript
|
||||
func (ts *TokenServer) SignBlindedTokenBatch(blindedTokens []BlindedToken, transcript *core.Transcript) (signedTokens []SignedToken, proof DLEQProof) {
|
||||
func (ts *TokenServer) SignBlindedTokenBatch(blindedTokens []BlindedToken, transcript *core.Transcript) SignedBatchWithProof {
|
||||
var signedTokens []SignedToken
|
||||
for _, bt := range blindedTokens {
|
||||
signedTokens = append(signedTokens, ts.SignBlindedToken(bt))
|
||||
}
|
||||
return signedTokens, ts.constructBatchProof(blindedTokens, signedTokens, transcript)
|
||||
return SignedBatchWithProof{signedTokens, ts.constructBatchProof(blindedTokens, signedTokens, transcript)}
|
||||
}
|
||||
|
||||
// constructBatchProof construct a batch proof that all the signed tokens have been signed correctly
|
||||
func (ts *TokenServer) constructBatchProof(blindedTokens []BlindedToken, signedTokens []SignedToken, transcript *core.Transcript) DLEQProof {
|
||||
transcript.AddToTranscript("X", new(ristretto.Point).SetBase().Bytes())
|
||||
transcript.AddToTranscript("Y", ts.Y.Bytes())
|
||||
transcript.AddToTranscript("P[]", []byte(fmt.Sprintf("%v", blindedTokens)))
|
||||
transcript.AddToTranscript("Q[]", []byte(fmt.Sprintf("%v", signedTokens)))
|
||||
transcript.NewProtocol(BatchProofProtocol)
|
||||
transcript.AddToTranscript(BatchProofX, new(ristretto.Element).Base().Encode(nil))
|
||||
transcript.AddToTranscript(BatchProofY, ts.Y.Encode(nil))
|
||||
transcript.AddToTranscript(BatchProofPVector, []byte(fmt.Sprintf("%v", blindedTokens)))
|
||||
transcript.AddToTranscript(BatchProofQVector, []byte(fmt.Sprintf("%v", signedTokens)))
|
||||
prng := transcript.CommitToPRNG("w")
|
||||
|
||||
M := new(ristretto.Point).SetZero()
|
||||
Z := new(ristretto.Point).SetZero()
|
||||
M := new(ristretto.Element).Zero()
|
||||
Z := new(ristretto.Element).Zero()
|
||||
|
||||
for i := range blindedTokens {
|
||||
c := prng.Next()
|
||||
M = new(ristretto.Point).Add(new(ristretto.Point).ScalarMult(blindedTokens[i].P, c), M)
|
||||
Z = new(ristretto.Point).Add(new(ristretto.Point).ScalarMult(signedTokens[i].Q, c), Z)
|
||||
M = new(ristretto.Element).Add(new(ristretto.Element).ScalarMult(c, blindedTokens[i].P), M)
|
||||
Z = new(ristretto.Element).Add(new(ristretto.Element).ScalarMult(c, signedTokens[i].Q), Z)
|
||||
}
|
||||
return DiscreteLogEquivalenceProof(ts.k, new(ristretto.Point).SetBase(), ts.Y, M, Z, transcript)
|
||||
return DiscreteLogEquivalenceProof(ts.k, new(ristretto.Element).Base(), ts.Y, M, Z, transcript)
|
||||
}
|
||||
|
||||
// IsValid returns true a SpentToken is valid and has never been spent before, false otherwise.
|
||||
func (ts *TokenServer) IsValid(token SpentToken, data []byte) bool {
|
||||
log.Debugf("data: [%s]", data)
|
||||
// SpendToken returns true a SpentToken is valid and has never been spent before, false otherwise.
|
||||
func (ts *TokenServer) SpendToken(token SpentToken, data []byte) error {
|
||||
ts.mutex.Lock()
|
||||
defer ts.mutex.Unlock() // We only want 1 client at a time redeeming tokens to prevent double-spends
|
||||
if ts.persistanceService == nil {
|
||||
if _, spent := ts.seen[hex.EncodeToString(token.T)]; spent {
|
||||
return false
|
||||
return fmt.Errorf("token: %v has already been spent", token)
|
||||
}
|
||||
Ht := sha3.Sum256(token.T)
|
||||
log.Debugf("token: %x", Ht)
|
||||
T := new(ristretto.Point).SetElligator(&Ht)
|
||||
W := new(ristretto.Point).ScalarMult(T, ts.k)
|
||||
key := sha3.Sum256(append(token.T, W.Bytes()...))
|
||||
} 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[:])
|
||||
K := mac.Sum(data)
|
||||
log.Debugf("mac: \n%x\nK:%x\n", token.MAC, K)
|
||||
result := hmac.Equal(token.MAC, K)
|
||||
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 result
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("token: %v is invalid and/or has not been signed by this service", token)
|
||||
}
|
||||
|
|
28
service.go
28
service.go
|
@ -9,7 +9,6 @@ import (
|
|||
"golang.org/x/crypto/ed25519"
|
||||
"golang.org/x/crypto/nacl/secretbox"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
||||
|
@ -19,7 +18,7 @@ type Service interface {
|
|||
Connect(hostname string, application Application) (bool, error)
|
||||
Listen(application Application) error
|
||||
GetConnection(connectionID string) (Connection, error)
|
||||
WaitForCapabilityOrClose(connectionID string, capability string) (Connection, error)
|
||||
WaitForCapabilityOrClose(connectionID string, capability Capability) (Connection, error)
|
||||
Shutdown()
|
||||
}
|
||||
|
||||
|
@ -30,19 +29,20 @@ type Connection interface {
|
|||
ID() *primitives.Identity
|
||||
Expect() []byte
|
||||
SetHostname(hostname string)
|
||||
HasCapability(name string) bool
|
||||
SetCapability(name string)
|
||||
HasCapability(name Capability) bool
|
||||
SetCapability(name Capability)
|
||||
SetEncryptionKey(key [32]byte)
|
||||
Send(message []byte)
|
||||
Close()
|
||||
App() Application
|
||||
SetApp(application Application)
|
||||
IsClosed() bool
|
||||
}
|
||||
|
||||
// Connection defines a Tapir Connection
|
||||
type connection struct {
|
||||
hostname string
|
||||
conn net.Conn
|
||||
conn io.ReadWriteCloser
|
||||
capabilities sync.Map
|
||||
encrypted bool
|
||||
key [32]byte
|
||||
|
@ -54,7 +54,7 @@ type connection struct {
|
|||
}
|
||||
|
||||
// NewConnection creates a new Connection
|
||||
func NewConnection(id *primitives.Identity, hostname string, outbound bool, conn net.Conn, app Application) Connection {
|
||||
func NewConnection(id *primitives.Identity, hostname string, outbound bool, conn io.ReadWriteCloser, app Application) Connection {
|
||||
connection := new(connection)
|
||||
connection.hostname = hostname
|
||||
connection.conn = conn
|
||||
|
@ -76,6 +76,11 @@ func (c *connection) App() Application {
|
|||
return c.app
|
||||
}
|
||||
|
||||
// App returns the overarching application using this Connection.
|
||||
func (c *connection) SetApp(application Application) {
|
||||
c.app = application
|
||||
}
|
||||
|
||||
// Hostname returns the hostname of the connection (if the connection has not been authorized it will return the
|
||||
// temporary hostname identifier)
|
||||
func (c *connection) Hostname() string {
|
||||
|
@ -100,13 +105,13 @@ func (c *connection) SetHostname(hostname string) {
|
|||
}
|
||||
|
||||
// SetCapability sets a capability on the connection
|
||||
func (c *connection) SetCapability(name string) {
|
||||
func (c *connection) SetCapability(name Capability) {
|
||||
log.Debugf("[%v -- %v] Setting Capability %v", c.identity.Hostname(), c.hostname, name)
|
||||
c.capabilities.Store(name, true)
|
||||
}
|
||||
|
||||
// HasCapability checks if the connection has a given capability
|
||||
func (c *connection) HasCapability(name string) bool {
|
||||
func (c *connection) HasCapability(name Capability) bool {
|
||||
_, ok := c.capabilities.Load(name)
|
||||
return ok
|
||||
}
|
||||
|
@ -142,9 +147,12 @@ func (c *connection) Expect() []byte {
|
|||
return []byte{}
|
||||
}
|
||||
}
|
||||
len, _ := binary.Uvarint(buffer[0:2])
|
||||
length, _ := binary.Uvarint(buffer[0:2])
|
||||
if length+2 >= uint64(c.MaxLength) {
|
||||
return []byte{}
|
||||
}
|
||||
//cplog.Debugf("[%v -> %v] Wire Receive: (%d) %x", c.hostname, c.ID.Hostname(), len, buffer)
|
||||
return buffer[2 : len+2]
|
||||
return buffer[2 : length+2]
|
||||
}
|
||||
|
||||
// SetEncryptionKey turns on application-level encryption on the connection using the given key.
|
||||
|
|
|
@ -3,9 +3,12 @@
|
|||
set -e
|
||||
pwd
|
||||
go test ${1} -coverprofile=applications.cover.out -v ./applications
|
||||
go test ${1} -coverprofile=applications.tokenboard.cover.out -v ./applications/tokenboard
|
||||
go test ${1} -coverprofile=primitives.cover.out -v ./primitives
|
||||
go test ${1} -coverprofile=primitives.auditable.cover.out -v ./primitives/auditable
|
||||
go test ${1} -coverprofile=primitives.core.cover.out -v ./primitives/core
|
||||
go test ${1} -coverprofile=primitives.privacypass.cover.out -v ./primitives/privacypass
|
||||
go test -bench "BenchmarkAuditableStore" -benchtime 1000x primitives/auditable/*.go
|
||||
echo "mode: set" > coverage.out && cat *.cover.out | grep -v mode: | sort -r | \
|
||||
awk '{if($1 != last) {print $0;last=$1}}' >> coverage.out
|
||||
rm -rf *.cover.out
|
||||
|
|
Loading…
Reference in New Issue