From 5b64c2d708fdb4b5e6a50561fd1634dde0f11e2c Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Sat, 14 Sep 2019 16:44:19 -0700 Subject: [PATCH 1/8] Auditable Store --- application.go | 5 + applications/auth.go | 8 +- applications/transcript_app.go | 27 +++++ primitives/auditablestore.go | 106 ++++++++++++++++++ primitives/auditablestore_test.go | 39 +++++++ primitives/bloom.go | 34 +++--- primitives/bloom_test.go | 24 ++++ primitives/{ => core}/transcript.go | 34 +++++- primitives/core/transcript_test.go | 28 +++++ primitives/dlog.go | 71 ++++++++++++ primitives/identity.go | 7 +- primitives/identity_test.go | 17 +++ primitives/privacypass/dlogeq.go | 69 ++++++++++++ primitives/privacypass/token.go | 98 ++++++++++++++++ primitives/privacypass/token_test.go | 63 +++++++++++ primitives/privacypass/tokenserver.go | 74 ++++++++++++ primitives/time.go | 21 ---- testing/tapir_integration_test.go | 8 +- ...tapir_malicious_remote_integration_test.go | 4 +- testing/tests.sh | 3 + 20 files changed, 691 insertions(+), 49 deletions(-) create mode 100644 applications/transcript_app.go create mode 100644 primitives/auditablestore.go create mode 100644 primitives/auditablestore_test.go create mode 100644 primitives/bloom_test.go rename primitives/{ => core}/transcript.go (64%) create mode 100644 primitives/core/transcript_test.go create mode 100644 primitives/dlog.go create mode 100644 primitives/identity_test.go create mode 100644 primitives/privacypass/dlogeq.go create mode 100644 primitives/privacypass/token.go create mode 100644 primitives/privacypass/token_test.go create mode 100644 primitives/privacypass/tokenserver.go delete mode 100644 primitives/time.go diff --git a/application.go b/application.go index 505ac1f..994e78b 100644 --- a/application.go +++ b/application.go @@ -1,7 +1,12 @@ package tapir +import ( + "cwtch.im/tapir/primitives/core" +) + // Application defines the interface for all Tapir Applications type Application interface { NewInstance() Application Init(connection Connection) + Transcript() *core.Transcript } diff --git a/applications/auth.go b/applications/auth.go index 5763232..e9da438 100644 --- a/applications/auth.go +++ b/applications/auth.go @@ -21,6 +21,7 @@ const AuthCapability = "AUTH" // AuthApp is the concrete Application type that handles Authentication type AuthApp struct { + TranscriptApp } // NewInstance creates a new instance of the AuthApp @@ -30,7 +31,8 @@ func (ea AuthApp) NewInstance() tapir.Application { // Init runs the entire AuthApp protocol, at the end of the protocol either the connection is granted AUTH capability // or the connection is closed. -func (ea AuthApp) Init(connection tapir.Connection) { +func (ea *AuthApp) Init(connection tapir.Connection) { + ea.TranscriptApp.Init(connection) longTermPubKey := ed25519.PublicKey(connection.ID().PublicKeyBytes()) ephemeralIdentity, _ := primitives.InitializeEphemeralIdentity() authMessage := AuthMessage{LongTermPublicKey: longTermPubKey, EphemeralPublicKey: ephemeralIdentity.PublicKey()} @@ -80,8 +82,8 @@ func (ea AuthApp) Init(connection tapir.Connection) { } // Derive a challenge from the transcript of the public parameters of this authentication protocol - var transcript *primitives.Transcript - transcript = primitives.NewTranscript("tapir-auth-" + outboundHostname + "-" + inboundHostname) + transcript := ea.Transcript() + transcript.AddToTranscript("auth-protocol", []byte(outboundHostname+"-"+inboundHostname)) transcript.AddToTranscript("outbound-challenge", outboundAuthMessage) transcript.AddToTranscript("inbound-challenge", inboundAuthMessage) challengeBytes := transcript.CommitToTranscript("3dh-auth-challenge") diff --git a/applications/transcript_app.go b/applications/transcript_app.go new file mode 100644 index 0000000..1b6c8ad --- /dev/null +++ b/applications/transcript_app.go @@ -0,0 +1,27 @@ +package applications + +import ( + "cwtch.im/tapir" + "cwtch.im/tapir/primitives/core" +) + +// TranscriptApp defines a Tapir Meta=App which provides a global cryptographic transcript +type TranscriptApp struct { + transcript *core.Transcript +} + +// NewInstance creates a new TranscriptApp +func (TranscriptApp) NewInstance() tapir.Application { + ta := new(TranscriptApp) + return ta +} + +// Init initializes the cryptographic transcript +func (ta *TranscriptApp) Init(connection tapir.Connection) { + ta.transcript = core.NewTranscript("tapir-transcript") +} + +// Transcript returns a pointer to the cryptographic transcript +func (ta *TranscriptApp) Transcript() *core.Transcript { + return ta.transcript +} diff --git a/primitives/auditablestore.go b/primitives/auditablestore.go new file mode 100644 index 0000000..df839f6 --- /dev/null +++ b/primitives/auditablestore.go @@ -0,0 +1,106 @@ +package primitives + +import ( + "crypto/subtle" + "cwtch.im/tapir/primitives/core" + "encoding/base64" + "errors" + "golang.org/x/crypto/ed25519" +) + +// SignedProof encapsulates a signed proof +type SignedProof []byte + +// Message encapsulates a message for more readable code. +type Message []byte + +// State defines an array of messages. +type State struct { + message []Message +} + +// AuditableStore defines a cryptographically secure & auditable transcript of messages sent from multiple +// unrelated clients to a server. +type AuditableStore struct { + state State + identity Identity + transcript *core.Transcript + latestCommit []byte + commits map[string]bool +} + +// Init initializes an auditable store +func (as *AuditableStore) Init(identity Identity) { + as.identity = identity + as.transcript = core.NewTranscript("auditable-data-store") + as.commits = make(map[string]bool) +} + +// Add adds a message to the auditable store +func (as *AuditableStore) Add(message Message, latestCommit []byte) ([]byte, SignedProof, error) { + if subtle.ConstantTimeCompare(latestCommit, as.latestCommit) == 1 { + as.state.message = append(as.state.message, message) + as.transcript.AddToTranscript("new-message", message) + as.latestCommit = as.identity.Sign(as.transcript.CommitToTranscript("commit")) + return as.latestCommit, as.identity.Sign(as.latestCommit), nil + } + // this prevents multiple clients updating at the same time and will likely cause retry storms. + return nil, nil, errors.New("attempt to append out of date transcript") +} + +// GetState returns the current auditable state +func (as *AuditableStore) GetState() (State, []byte, SignedProof) { + return as.state, as.latestCommit, as.identity.Sign(as.latestCommit) +} + +// MergeState merges a given state onto our state, first verifying that the two transcripts align +func (as *AuditableStore) MergeState(state State, signedStateProof SignedProof, key ed25519.PublicKey) error { + next := len(as.state.message) + for _, m := range state.message[next:] { + as.state.message = append(as.state.message, m) + + // We reconstruct the transcript + as.transcript.AddToTranscript("new-message", m) + as.latestCommit = as.identity.Sign(as.transcript.CommitToTranscript("commit")) + as.commits[base64.StdEncoding.EncodeToString(as.latestCommit)] = true + } + + // 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(key, as.latestCommit, signedStateProof) == false { + return errors.New("state is not consistent, the server is malicious") + } + return nil +} + +// VerifyFraudProof - the main idea behind this is as follows: +// +// Every update requires the server to sign, and thus commit to, a transcript +// Clients reconstruct the transcript via MergeState, as such clients can keep track of every commit. +// if a client can present a signed transcript commit from the server that other clients do not have, it is proof +// that either 1) they are out of sync with the server or 2) the server is presenting different transcripts to different people +// +// If, after syncing, the FraudProof still validates, then the server must be malicious. +// the information revealed by publicizing a fraud proof is minimal it only reveals the inconsistent transcript commit +// and not the cause (which could be reordered messages, dropped messages, additional messages or any combination) +func (as *AuditableStore) VerifyFraudProof(commit []byte, signedFraudProof SignedProof, key ed25519.PublicKey) (bool, error) { + + if ed25519.Verify(key, commit, 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(commit)] + if !exists { + // We have a message signed by the server which verifies that a message was inserted into the state at a given index + // However this directly contradicts our version of the state. + // There is still a possibility that we are out of sync with the server and that new messages have since been added + // We assume that the caller has first Merged the most recent state. + return true, nil + } + + return false, nil +} diff --git a/primitives/auditablestore_test.go b/primitives/auditablestore_test.go new file mode 100644 index 0000000..de24f47 --- /dev/null +++ b/primitives/auditablestore_test.go @@ -0,0 +1,39 @@ +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"), as.latestCommit) + state, _, proof := as.GetState() + + if vs.MergeState(state, proof, serverID.PublicKey()) != nil { + t.Fatalf("Fraud Proof Failed on Honest Proof") + } + + commit, fraudProof, _ := as.Add([]byte("Hello World 2"), as.latestCommit) + + // If you comment these out it simulates a lying server. + state, _, proof = as.GetState() + if vs.MergeState(state, proof, serverID.PublicKey()) != nil { + t.Fatalf("Fraud Proof Failed on Honest Proof") + } + + fraud, err := vs.VerifyFraudProof(commit, fraudProof, serverID.PublicKey()) + + if err != nil { + t.Fatalf("Error validated fraud proof: %v", err) + } + + if fraud { + t.Fatalf("Technically a fraud, but the client hasn't updated yet") + } +} diff --git a/primitives/bloom.go b/primitives/bloom.go index 3104a81..e730f85 100644 --- a/primitives/bloom.go +++ b/primitives/bloom.go @@ -2,6 +2,8 @@ package primitives import ( "crypto/sha256" + "math" + "math/big" "sync" ) @@ -12,32 +14,30 @@ type BloomFilter struct { } // Init constructs a bloom filter of size m -func (bf *BloomFilter) Init(m int16) { +func (bf *BloomFilter) Init(m int64) { bf.B = make([]bool, m) + } // Hash transforms a message to a set of bit flips -// Supports up to m == 65535 func (bf *BloomFilter) Hash(msg []byte) []int { - hash := sha256.Sum256(msg) - pos1a := (int(hash[0]) + int(hash[1]) + int(hash[2]) + int(hash[3])) % 0xFF - pos1b := (int(hash[4]) + int(hash[5]) + int(hash[6]) + int(hash[7])) % 0xFF - pos1 := ((pos1a << 8) + pos1b) & (0xFFFF % len(bf.B)) + // Not the fastest hash function ever, but cryptographic security is more important than speed. + hash1 := sha256.Sum256(append([]byte("h1"), msg...)) + hash2 := sha256.Sum256(append([]byte("h2"), msg...)) + hash3 := sha256.Sum256(append([]byte("h3"), msg...)) + hash4 := sha256.Sum256(append([]byte("h4"), msg...)) - pos2a := (int(hash[8]) + int(hash[9]) + int(hash[10]) + int(hash[11])) % 0xFF - pos2b := (int(hash[12]) + int(hash[13]) + int(hash[14]) + int(hash[15])) % 0xFF - pos2 := ((pos2a << 8) + pos2b) & (0xFFFF % len(bf.B)) + m := int64(len(bf.B)) + // Number of bytes needed to pick a position from [0,m) + B := int(math.Ceil(math.Log2(float64(m)) / 8.0)) - pos3a := (int(hash[16]) + int(hash[17]) + int(hash[18]) + int(hash[19])) % 0xFF - pos3b := (int(hash[20]) + int(hash[21]) + int(hash[22]) + int(hash[23])) % 0xFF - pos3 := ((pos3a << 8) + pos3b) & (0xFFFF % len(bf.B)) + p1 := big.NewInt(0).SetBytes(hash1[:B]).Int64() + p2 := big.NewInt(0).SetBytes(hash2[:B]).Int64() + p3 := big.NewInt(0).SetBytes(hash3[:B]).Int64() + p4 := big.NewInt(0).SetBytes(hash4[:B]).Int64() - pos4a := (int(hash[24]) + int(hash[25]) + int(hash[26]) + int(hash[27])) % 0xFF - pos4b := (int(hash[28]) + int(hash[29]) + int(hash[30]) + int(hash[31])) % 0xFF - pos4 := ((pos4a << 8) + pos4b) & (0xFFFF % len(bf.B)) - - return []int{pos1, pos2, pos3, pos4} + return []int{int(p1), int(p2), int(p3), int(p4)} } // Insert updates the BloomFilter (suitable for concurrent use) diff --git a/primitives/bloom_test.go b/primitives/bloom_test.go new file mode 100644 index 0000000..ecda1ed --- /dev/null +++ b/primitives/bloom_test.go @@ -0,0 +1,24 @@ +package primitives + +import ( + "strconv" + "testing" +) + +func TestBloomFilter_Insert(t *testing.T) { + bf := new(BloomFilter) + bf.Init(256) + + fp := 0 + for i := 0; i < 256; i++ { + input := []byte("test" + strconv.Itoa(256+i)) + if bf.Check(input) { + t.Log("False Positive!") + fp++ + } + bf.Insert(input) + } + + t.Logf("Num false positives %v %v%%", fp, (float64(fp)/256.0)*100) + +} diff --git a/primitives/transcript.go b/primitives/core/transcript.go similarity index 64% rename from primitives/transcript.go rename to primitives/core/transcript.go index ebb4bed..f2d6450 100644 --- a/primitives/transcript.go +++ b/primitives/core/transcript.go @@ -1,9 +1,11 @@ -package primitives +package core import ( "fmt" + "github.com/bwesterb/go-ristretto" "golang.org/x/crypto/sha3" "hash" + "io" ) // Transcript implements a transcript of a public coin argument. @@ -49,3 +51,33 @@ func (t *Transcript) CommitToTranscript(label string) []byte { t.AddToTranscript(label, b) return b } + +// PRNG defines a psuedorandom number generator +type PRNG struct { + prng io.Reader +} + +// Next returns the next "random" scalar from the PRNG +func (prng *PRNG) Next() *ristretto.Scalar { + buf := [32]byte{} + io.ReadFull(prng.prng, buf[:]) + return new(ristretto.Scalar).SetBytes(&buf) +} + +// 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) + prng := sha3.NewShake256() + prng.Write(b) + return PRNG{prng: prng} +} + +// 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) +} diff --git a/primitives/core/transcript_test.go b/primitives/core/transcript_test.go new file mode 100644 index 0000000..7f7016d --- /dev/null +++ b/primitives/core/transcript_test.go @@ -0,0 +1,28 @@ +package core + +import ( + "testing" +) + +func TestNewTranscript(t *testing.T) { + + // Some very basic integrity checking + transcript := NewTranscript("label") + + transcript.AddToTranscript("action", []byte("test data")) + + if transcript.OutputTranscriptToAudit() != transcript.OutputTranscriptToAudit() { + t.Fatalf("Multiple Audit Calls should not impact underlying Transcript") + } + t.Logf("%v", transcript.OutputTranscriptToAudit()) + t.Logf("%v", transcript.CommitToTranscript("first commit")) + t.Logf("%v", transcript.OutputTranscriptToAudit()) + t.Logf("%v", transcript.CommitToTranscript("second commit")) + t.Logf("%v", transcript.OutputTranscriptToAudit()) + + transcript.AddToTranscript("action", []byte("test data")) + + t.Logf("%v", transcript.CommitToTranscript("third commit")) + t.Logf("%v", transcript.OutputTranscriptToAudit()) + +} diff --git a/primitives/dlog.go b/primitives/dlog.go new file mode 100644 index 0000000..f89b7c0 --- /dev/null +++ b/primitives/dlog.go @@ -0,0 +1,71 @@ +package primitives + +import ( + "cwtch.im/tapir/primitives/core" + "github.com/bwesterb/go-ristretto" +) + +// DLProof Encapsulates a Discrete Log / Schnorr Proof +// Note that these parameters are read-only. +type DLProof struct { + V, A ristretto.Point + R ristretto.Scalar +} + +// DiscreteLogProof - Proof of Knowledge of Exponent +// Given V = xG +// Peggy: z := choose randomly from Zq +// A := zG +// c := H(transcript(G,V,A)) mod q +// r := (z + cx) mod q +// +// Sends A,r,V to Vicky +func DiscreteLogProof(x ristretto.Scalar, v ristretto.Point, transcript *core.Transcript) (proof DLProof) { + + transcript.AddToTranscript("G", new(ristretto.Point).SetBase().Bytes()) + + // We bind the proof to our public V + proof.V = v + transcript.AddToTranscript("V", proof.V.Bytes()) + + // Generate a random z + // A := zG + z := new(ristretto.Scalar).Rand() + proof.A = *new(ristretto.Point).ScalarMultBase(z) + transcript.AddToTranscript("A", proof.A.Bytes()) + + // Derive Challenge + c := transcript.CommitToTranscriptScalar("c") + + // r := (z + cx) mod p + cx := new(ristretto.Scalar).Mul(c, &x) + proof.R = *new(ristretto.Scalar).Add(z, cx) + + return +} + +// VerifyDiscreteLogProof validates a given Schnorr Proof +// Vicky gets A,r,V from Peggy +// Vicky computes c := H(transcript(G,V,A)) mod q +// Vicky checks rG := A + cV +// rG ?= zG + cV +// (z+cx)G ?= zG + cV +// ?= zG + cxG +// Thus demonstrating that Peggy knows the discrete log to V +func VerifyDiscreteLogProof(proof DLProof, transcript *core.Transcript) bool { + + transcript.AddToTranscript("G", new(ristretto.Point).SetBase().Bytes()) + transcript.AddToTranscript("V", proof.V.Bytes()) + transcript.AddToTranscript("A", proof.A.Bytes()) + c := transcript.CommitToTranscriptScalar("c") + + // Compute left hand side + lhs := new(ristretto.Point).ScalarMultBase(&proof.R) + + // Compute right hand side + cV := new(ristretto.Point).ScalarMult(&proof.V, c) + rhs := new(ristretto.Point).Add(&proof.A, cV) + + // Result of verification: lhs ?= rhs + return lhs.Equals(rhs) +} diff --git a/primitives/identity.go b/primitives/identity.go index 9307510..abb6a4f 100644 --- a/primitives/identity.go +++ b/primitives/identity.go @@ -41,7 +41,7 @@ func (i *Identity) PublicKey() ed25519.PublicKey { return *i.edpubk } -// EDH performs a diffie-helman operation on this identities private key with the given public key. +// EDH performs a diffie-hellman operation on this identities private key with the given public key. func (i *Identity) EDH(key ed25519.PublicKey) []byte { secret := utils.EDH(*i.edpk, key) return secret[:] @@ -51,3 +51,8 @@ func (i *Identity) EDH(key ed25519.PublicKey) []byte { func (i *Identity) Hostname() string { return utils.GetTorV3Hostname(*i.edpubk) } + +// Sign produces a signature for a given message attributable to the given identity +func (i *Identity) Sign(input []byte) []byte { + return ed25519.Sign(*i.edpk, input) +} diff --git a/primitives/identity_test.go b/primitives/identity_test.go new file mode 100644 index 0000000..49f5e23 --- /dev/null +++ b/primitives/identity_test.go @@ -0,0 +1,17 @@ +package primitives + +import ( + "testing" +) + +func TestIdentity_EDH(t *testing.T) { + + id1, _ := InitializeEphemeralIdentity() + id2, _ := InitializeEphemeralIdentity() + + k1 := id1.EDH(id2.PublicKey()) + k2 := id2.EDH(id1.PublicKey()) + + t.Logf("k1: %x\nk2: %x\n", k1, k2) + +} diff --git a/primitives/privacypass/dlogeq.go b/primitives/privacypass/dlogeq.go new file mode 100644 index 0000000..89caa1e --- /dev/null +++ b/primitives/privacypass/dlogeq.go @@ -0,0 +1,69 @@ +package privacypass + +import ( + "cwtch.im/tapir/primitives/core" + "github.com/bwesterb/go-ristretto" +) + +// 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 +type DLEQProof struct { + C *ristretto.Scalar + S *ristretto.Scalar +} + +// DiscreteLogEquivalenceProof constructs a valid DLEQProof for the given parameters and transcript +// Given P = kX, Q = kP, Y=kX +// Peggy: t := choose randomly from Zq +// A := tX +// B := tP +// c := H(transcript(X,Y,P,Q,A,B)) +// s := (t + ck) mod q +// +// Sends c,s to Vicky +func DiscreteLogEquivalenceProof(k *ristretto.Scalar, X *ristretto.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) + + 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()) + + c := transcript.CommitToTranscriptScalar("c") + s := new(ristretto.Scalar).Sub(t, new(ristretto.Scalar).Mul(c, k)) + return DLEQProof{c, s} +} + +// VerifyDiscreteLogEquivalenceProof verifies the DLEQ for the given parameters and transcript +// Given P = kX, Q = kP, Y=kX, and Proof = (c,s) +// Vicky: X' := sX +// Y' := cY +// P' := sP +// Q' := cQ +// A' = X'+Y' == sX + cY ?= sX + ckX == (s+ck)X == tX == A +// B' = P'+Q' == sP + cQ ?= sP + ckP == (s+ck)P == tP == B +// c' := H(transcript(X,Y,P,Q,A',B')) +// Tests c ?= c +func VerifyDiscreteLogEquivalenceProof(dleq DLEQProof, X *ristretto.Point, Y *ristretto.Point, P *ristretto.Point, Q *ristretto.Point, 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) + + A := new(ristretto.Point).Add(Xs, Yc) + B := new(ristretto.Point).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()) + + return transcript.CommitToTranscriptScalar("c").Equals(dleq.C) +} diff --git a/primitives/privacypass/token.go b/primitives/privacypass/token.go new file mode 100644 index 0000000..0935490 --- /dev/null +++ b/primitives/privacypass/token.go @@ -0,0 +1,98 @@ +package privacypass + +import ( + "crypto/hmac" + "crypto/rand" + "cwtch.im/tapir/primitives/core" + "fmt" + "github.com/bwesterb/go-ristretto" + "golang.org/x/crypto/sha3" +) + +// Token is an implementation of PrivacyPass +// Davidson A, Goldberg I, Sullivan N, Tankersley G, Valsorda F. Privacy pass: Bypassing internet challenges anonymously. Proceedings on Privacy Enhancing Technologies. 2018 Jun 1;2018(3):164-80. +type Token struct { + t []byte + r *ristretto.Scalar + W *ristretto.Point +} + +// BlindedToken encapsulates a Blinded Token +type BlindedToken struct { + P *ristretto.Point +} + +// SignedToken encapsulates a Signed (Blinded) Token +type SignedToken struct { + Q *ristretto.Point +} + +// SpentToken encapsulates the parameters needed to spend a Token +type SpentToken struct { + t []byte + MAC []byte +} + +// GenBlindedToken initializes the Token +// GenToken() & Blind() +func (t *Token) GenBlindedToken() BlindedToken { + t.t = make([]byte, 32) + rand.Read(t.t) + t.r = new(ristretto.Scalar).Rand() + + Ht := sha3.Sum256(t.t) + T := new(ristretto.Point).SetElligator(&Ht) + P := new(ristretto.Point).ScalarMult(T, t.r) + 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)) +} + +// 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()...)) + mac := hmac.New(sha3.New512, key[:]) + return SpentToken{t.t, mac.Sum(data)} +} + +// GenerateBlindedTokenBatch generates a batch of blinded tokens (and their unblinded equivalents) +func GenerateBlindedTokenBatch(num int) (tokens []*Token, blindedTokens []BlindedToken) { + for i := 0; i < num; i++ { + tokens = append(tokens, new(Token)) + blindedTokens = append(blindedTokens, tokens[i].GenBlindedToken()) + } + return +} + +// verifyBatchProof verifies a given batch proof (see also UnblindSignedTokenBatch) +func verifyBatchProof(dleq DLEQProof, Y *ristretto.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))) + prng := transcript.CommitToPRNG("w") + M := new(ristretto.Point).SetZero() + Z := new(ristretto.Point).SetZero() + 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) + } + return VerifyDiscreteLogEquivalenceProof(dleq, new(ristretto.Point).SetBase(), 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 { + verified := verifyBatchProof(proof, Y, blindedTokens, signedTokens, transcript) + if !verified { + return false + } + for i, t := range tokens { + t.unblindSignedToken(signedTokens[i]) + } + return true +} diff --git a/primitives/privacypass/token_test.go b/primitives/privacypass/token_test.go new file mode 100644 index 0000000..5e3d929 --- /dev/null +++ b/primitives/privacypass/token_test.go @@ -0,0 +1,63 @@ +package privacypass + +import ( + "cwtch.im/tapir/primitives/core" + "testing" +) + +func TestToken_SpendToken(t *testing.T) { + server := NewTokenServer() + + token := new(Token) + blindedToken := token.GenBlindedToken() + + signedToken := server.SignBlindedToken(blindedToken) + token.unblindSignedToken(signedToken) + + spentToken := token.SpendToken([]byte("Hello")) + + if server.IsValid(spentToken, []byte("Hello World")) == true { + t.Errorf("Token Should be InValid") + } + + if server.IsValid(spentToken, []byte("Hello")) == false { + t.Errorf("Token Should be Valid") + } + + if server.IsValid(spentToken, []byte("Hello")) == true { + t.Errorf("Token Should be Spent") + } +} + +func TestGenerateBlindedTokenBatch(t *testing.T) { + server := NewTokenServer() + + clientTranscript := core.NewTranscript("privacyPass") + serverTranscript := core.NewTranscript("privacyPass") + + tokens, blindedTokens := GenerateBlindedTokenBatch(10) + signedTokens, proof := server.SignBlindedTokenBatch(blindedTokens, serverTranscript) + + verified := UnblindSignedTokenBatch(tokens, blindedTokens, signedTokens, server.Y, proof, clientTranscript) + + if !verified { + t.Errorf("Something went wrong, the proof did not pass") + } + + // Attempt to Spend All the tokens + for _, token := range tokens { + spentToken := token.SpendToken([]byte("Hello")) + if server.IsValid(spentToken, []byte("Hello")) == false { + t.Errorf("Token Should be Valid") + } + } + + t.Logf("Client Transcript,: %s", clientTranscript.OutputTranscriptToAudit()) + t.Logf("Server Transcript,: %s", serverTranscript.OutputTranscriptToAudit()) + + wrongTranscript := core.NewTranscript("wrongTranscript") + verified = UnblindSignedTokenBatch(tokens, blindedTokens, signedTokens, server.Y, proof, wrongTranscript) + if verified { + t.Errorf("Something went wrong, the proof passed with wrong transcript: %s", wrongTranscript.OutputTranscriptToAudit()) + } +} diff --git a/primitives/privacypass/tokenserver.go b/primitives/privacypass/tokenserver.go new file mode 100644 index 0000000..48a9740 --- /dev/null +++ b/primitives/privacypass/tokenserver.go @@ -0,0 +1,74 @@ +package privacypass + +import ( + "crypto/hmac" + "cwtch.im/tapir/primitives/core" + "encoding/hex" + "fmt" + "github.com/bwesterb/go-ristretto" + "golang.org/x/crypto/sha3" +) + +// TokenServer implements a token server. +type TokenServer struct { + k *ristretto.Scalar + Y *ristretto.Point + seen map[string]bool +} + +// 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)} +} + +// SignBlindedToken calculates kP for the given BlindedToken P +func (ts *TokenServer) SignBlindedToken(bt BlindedToken) SignedToken { + Q := new(ristretto.Point).ScalarMult(bt.P, ts.k) + 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) { + for _, bt := range blindedTokens { + signedTokens = append(signedTokens, ts.SignBlindedToken(bt)) + } + return 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))) + prng := transcript.CommitToPRNG("w") + + M := new(ristretto.Point).SetZero() + Z := new(ristretto.Point).SetZero() + + 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) + } + return DiscreteLogEquivalenceProof(ts.k, new(ristretto.Point).SetBase(), ts.Y, M, Z, transcript) +} + +// IsValid returns true a SpentToken is valid and has never been spent before, false otherwise. +func (ts *TokenServer) IsValid(token SpentToken, data []byte) bool { + if _, spent := ts.seen[hex.EncodeToString(token.t)]; spent { + return false + } + Ht := sha3.Sum256(token.t) + T := new(ristretto.Point).SetElligator(&Ht) + W := new(ristretto.Point).ScalarMult(T, ts.k) + key := sha3.Sum256(append(token.t, W.Bytes()...)) + mac := hmac.New(sha3.New512, key[:]) + K := mac.Sum(data) + result := hmac.Equal(token.MAC, K) + if result == true { + ts.seen[hex.EncodeToString(token.t)] = true + } + return result +} diff --git a/primitives/time.go b/primitives/time.go deleted file mode 100644 index b957a46..0000000 --- a/primitives/time.go +++ /dev/null @@ -1,21 +0,0 @@ -package primitives - -import ( - "time" -) - -// TimeProvider is an interface used by services to timestamp events. Why not just have them use time.Now()? We want -// to be able to write tests that simulate behavior over several hours, and thus having an interface to abstract away -// time details for the services is very useful. -type TimeProvider interface { - GetCurrentTime() time.Time -} - -// OSTimeProvider provides a wrapper around time provider which simply provides the time as given by the operating system. -type OSTimeProvider struct { -} - -// GetCurrentTime returns the time provided by the OS -func (ostp OSTimeProvider) GetCurrentTime() time.Time { - return time.Now() -} diff --git a/testing/tapir_integration_test.go b/testing/tapir_integration_test.go index 65eae06..e77aa9c 100644 --- a/testing/tapir_integration_test.go +++ b/testing/tapir_integration_test.go @@ -21,12 +21,12 @@ type SimpleApp struct { } // NewInstance should always return a new instantiation of the application. -func (ea SimpleApp) NewInstance() tapir.Application { +func (ea *SimpleApp) NewInstance() tapir.Application { return new(SimpleApp) } // Init is run when the connection is first started. -func (ea SimpleApp) Init(connection tapir.Connection) { +func (ea *SimpleApp) Init(connection tapir.Connection) { // First run the Authentication App ea.AuthApp.Init(connection) @@ -76,7 +76,7 @@ func TestTapir(t *testing.T) { sg := new(sync.WaitGroup) sg.Add(1) go func() { - service.Listen(SimpleApp{}) + service.Listen(new(SimpleApp)) sg.Done() }() @@ -114,7 +114,7 @@ func genclient(acn connectivity.ACN) (tapir.Service, string) { // Client will Connect and launch it's own Echo App goroutine. func connectclient(client tapir.Service, key ed25519.PublicKey, group *sync.WaitGroup) { - client.Connect(utils.GetTorV3Hostname(key), SimpleApp{}) + client.Connect(utils.GetTorV3Hostname(key), new(SimpleApp)) // Once connected, it shouldn't take long to authenticate and run the application. So for the purposes of this demo // we will wait a little while then exit. diff --git a/testing/tapir_malicious_remote_integration_test.go b/testing/tapir_malicious_remote_integration_test.go index e18a56d..4262fff 100644 --- a/testing/tapir_malicious_remote_integration_test.go +++ b/testing/tapir_malicious_remote_integration_test.go @@ -40,7 +40,7 @@ func TestTapirMaliciousRemote(t *testing.T) { sg := new(sync.WaitGroup) sg.Add(1) go func() { - service.Listen(applications.AuthApp{}) + service.Listen(new(applications.AuthApp)) sg.Done() }() @@ -67,7 +67,7 @@ func TestTapirMaliciousRemote(t *testing.T) { // Client will Connect and launch it's own Echo App goroutine. func connectclientandfail(client tapir.Service, key ed25519.PublicKey, group *sync.WaitGroup, t *testing.T) { - client.Connect(utils.GetTorV3Hostname(key), applications.AuthApp{}) + client.Connect(utils.GetTorV3Hostname(key), new(applications.AuthApp)) // Once connected, it shouldn't take long to authenticate and run the application. So for the purposes of this demo // we will wait a little while then exit. diff --git a/testing/tests.sh b/testing/tests.sh index b3e6652..c9818a1 100755 --- a/testing/tests.sh +++ b/testing/tests.sh @@ -3,6 +3,9 @@ set -e pwd go test ${1} -coverprofile=applications.cover.out -v ./applications +go test ${1} -coverprofile=primitives.cover.out -v ./primitives +go test ${1} -coverprofile=primitives.core.cover.out -v ./primitives/core +go test ${1} -coverprofile=primitives.privacypass.cover.out -v ./primitives/privacypass 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 From 345d11f506ba05a9f4b3a3e3f004b777a32c9264 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Sat, 14 Sep 2019 22:50:06 -0700 Subject: [PATCH 2/8] First cut of Token Board --- .gitignore | 1 + applications/tokenboard.go | 192 ++++++++++++++++++++ applications/tokenboard_integration_test.go | 118 ++++++++++++ primitives/auditablestore.go | 71 +++++--- primitives/auditablestore_test.go | 14 +- primitives/privacypass/token.go | 10 +- primitives/privacypass/token_test.go | 2 + primitives/privacypass/tokenserver.go | 24 ++- 8 files changed, 389 insertions(+), 43 deletions(-) create mode 100644 applications/tokenboard.go create mode 100644 applications/tokenboard_integration_test.go diff --git a/.gitignore b/.gitignore index 4ede555..003fb79 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ vendor/ /tor/ coverage.out /testing/tor/ +/applications/tor/ diff --git a/applications/tokenboard.go b/applications/tokenboard.go new file mode 100644 index 0000000..d471e8a --- /dev/null +++ b/applications/tokenboard.go @@ -0,0 +1,192 @@ +package applications + +import ( + "cwtch.im/tapir" + "cwtch.im/tapir/primitives" + "cwtch.im/tapir/primitives/privacypass" + "encoding/json" + "git.openprivacy.ca/openprivacy/libricochet-go/log" +) + +// TokenBoardApp defines a Tapir Meta=App which provides a global cryptographic transcript +type TokenBoardApp struct { + AuthApp + connection tapir.Connection + TokenService *privacypass.TokenServer + AuditableStore *primitives.AuditableStore + paymentHandler privacypass.TokenPaymentHandler + handler TokenBoardAppHandler +} + +// TokenBoardAppHandler allows clients to react to specific events. +type TokenBoardAppHandler interface { + HandleNewMessages(previousLastCommit []byte) +} + +// NewTokenBoardClient generates a new Client for Token Board +func NewTokenBoardClient(store *primitives.AuditableStore, handler TokenBoardAppHandler, paymentHandler privacypass.TokenPaymentHandler) tapir.Application { + tba := new(TokenBoardApp) + tba.TokenService = nil + tba.AuditableStore = store + tba.handler = handler + tba.paymentHandler = paymentHandler + return tba +} + +// NewTokenBoardServer generates new Server for Token Board +func NewTokenBoardServer(tokenService *privacypass.TokenServer, store *primitives.AuditableStore) tapir.Application { + tba := new(TokenBoardApp) + tba.TokenService = tokenService + tba.AuditableStore = store + return tba +} + +// NewInstance creates a new TokenBoardApp +func (ta *TokenBoardApp) NewInstance() tapir.Application { + tba := new(TokenBoardApp) + tba.TokenService = ta.TokenService + tba.AuditableStore = ta.AuditableStore + tba.handler = ta.handler + tba.paymentHandler = ta.paymentHandler + return tba +} + +// Init initializes the cryptographic TokenBoardApp +func (ta *TokenBoardApp) Init(connection tapir.Connection) { + ta.AuthApp.Init(connection) + + if connection.HasCapability(AuthCapability) { + ta.connection = connection + // If we are a server, now we can start listening for inbound messages + if ta.connection.IsOutbound() { + go ta.listen(ta.clientSwitch) + } else { + go ta.listen(ta.serverSwitch) + } + return + } + connection.Close() +} + +// TokenBoardMessage encapsulates the application protocol +type TokenBoardMessage struct { + MessageType string + PostRequest PostRequest `json:",omitempty"` + PostResult PostResult `json:",omitempty"` + ReplayRequest ReplayRequest `json:",omitempty"` + ReplayResponse ReplayResponse `json:",omitempty"` +} + +// ReplayRequest requests a reply from the given Commit +type ReplayRequest struct { + LastCommit []byte +} + +// PostRequest requests to post the message to the board with the given token +type PostRequest struct { + Token privacypass.SpentToken + Message primitives.Message +} + +// PostResult returns the success of a given post attempt +type PostResult struct { + Success bool + Proof primitives.SignedProof +} + +// ReplayResponse is sent by the server before a stream of replayed messages +type ReplayResponse struct { + NumMessages int +} + +func (ta *TokenBoardApp) clientSwitch(message TokenBoardMessage) { + switch message.MessageType { + case "PostResult": + log.Debugf("Post result: %x", message.PostResult.Proof) + case "ReplayResponse": + var state primitives.State + log.Debugf("Replaying %v Messages...", message.ReplayResponse.NumMessages) + lastCommit := ta.AuditableStore.LatestCommit + for i := 0; i < message.ReplayResponse.NumMessages; i++ { + message := ta.connection.Expect() + state.Messages = append(state.Messages, message) + } + data := ta.connection.Expect() + var signedProof primitives.SignedProof + json.Unmarshal(data, &signedProof) + err := ta.AuditableStore.MergeState(state, signedProof) + if err == nil { + log.Debugf("Successfully updated Auditable Store") + ta.handler.HandleNewMessages(lastCommit) + } else { + log.Debugf("Error updating Auditable Store %v", err) + } + } +} + +func (ta *TokenBoardApp) serverSwitch(message TokenBoardMessage) { + switch message.MessageType { + case "PostRequest": + postrequest := message.PostRequest + log.Debugf("Received a Post Message Request: %x %x", postrequest.Token, postrequest.Message) + ta.postMessageRequest(postrequest.Token, postrequest.Message) + case "ReplayRequest": + state, proof := ta.AuditableStore.GetState() + response, _ := json.Marshal(TokenBoardMessage{MessageType: "ReplayResponse", ReplayResponse: ReplayResponse{len(state.Messages)}}) + ta.connection.Send(response) + for _, message := range state.Messages { + ta.connection.Send(message) + } + data, _ := json.Marshal(proof) + ta.connection.Send(data) + } +} + +func (ta *TokenBoardApp) listen(switchFn func(TokenBoardMessage)) { + for { + data := ta.connection.Expect() + if len(data) == 0 { + return // connection is closed + } + + var message TokenBoardMessage + json.Unmarshal(data, &message) + log.Debugf("Received a Message: %v", message) + switchFn(message) + } +} + +// Replay posts a Replay Message to the server. +func (ta *TokenBoardApp) Replay() { + data, _ := json.Marshal(TokenBoardMessage{MessageType: "ReplayRequest"}) + ta.connection.Send(data) +} + +// PurchaseTokens purchases the given number of tokens from the server (using the provided payment handler) +func (ta *TokenBoardApp) PurchaseTokens(num int) { + ta.paymentHandler.MakePayment(num) +} + +// Post sends a Post Request to the server +func (ta *TokenBoardApp) Post(message primitives.Message) bool { + token, err := ta.paymentHandler.NextToken(message) + if err == nil { + data, _ := json.Marshal(TokenBoardMessage{MessageType: "PostRequest", PostRequest: PostRequest{Token: token, Message: message}}) + ta.connection.Send(data) + return true + } + return false +} + +func (ta *TokenBoardApp) postMessageRequest(token privacypass.SpentToken, message primitives.Message) { + if ta.TokenService.IsValid(token, message) { + log.Debugf("Token is valid") + signedproof := ta.AuditableStore.Add(message) + data, _ := json.Marshal(TokenBoardMessage{MessageType: "PostResult", PostResult: PostResult{true, signedproof}}) + ta.connection.Send(data) + } else { + log.Debugf("Attempt to spend an invalid token") + data, _ := json.Marshal(TokenBoardMessage{MessageType: "PostResult", PostResult: PostResult{false, primitives.SignedProof{}}}) + ta.connection.Send(data) + } +} diff --git a/applications/tokenboard_integration_test.go b/applications/tokenboard_integration_test.go new file mode 100644 index 0000000..285585d --- /dev/null +++ b/applications/tokenboard_integration_test.go @@ -0,0 +1,118 @@ +package applications + +import ( + "cwtch.im/tapir" + "cwtch.im/tapir/networks/tor" + "cwtch.im/tapir/primitives" + "cwtch.im/tapir/primitives/core" + "cwtch.im/tapir/primitives/privacypass" + "errors" + "git.openprivacy.ca/openprivacy/libricochet-go/connectivity" + "git.openprivacy.ca/openprivacy/libricochet-go/log" + "runtime" + "sync" + "testing" + "time" +) + +type Handler struct { + Store *primitives.AuditableStore +} + +func (h Handler) HandleNewMessages(previousLastCommit []byte) { + log.Debugf("Handling Messages After %x", previousLastCommit) + messages := h.Store.GetMessagesAfter(previousLastCommit) + for _, message := range messages { + log.Debugf("Message %s", message) + } +} + +type FreePaymentHandler struct { + tokens []*privacypass.Token + TokenService *privacypass.TokenServer +} + +func (fph *FreePaymentHandler) MakePayment(int) { + tokens, blindedTokens := privacypass.GenerateBlindedTokenBatch(10) + // Obtained some signed tokens, in reality these would be bought and paid for through some other mechanism. + clientTranscript := core.NewTranscript("privacyPass") + serverTranscript := core.NewTranscript("privacyPass") + + signedTokens, proof := fph.TokenService.SignBlindedTokenBatch(blindedTokens, serverTranscript) + privacypass.UnblindSignedTokenBatch(tokens, blindedTokens, signedTokens, fph.TokenService.Y, proof, clientTranscript) + fph.tokens = append(fph.tokens, tokens...) +} + +func (fph *FreePaymentHandler) NextToken(data []byte) (privacypass.SpentToken, error) { + if len(fph.tokens) == 0 { + return privacypass.SpentToken{}, errors.New("No more tokens") + } + token := fph.tokens[0] + fph.tokens = fph.tokens[1:] + return token.SpendToken(data), nil +} + +func TestTokenBoardApp(t *testing.T) { + // numRoutinesStart := runtime.NumGoroutine() + log.SetLevel(log.LevelDebug) + log.Infof("Number of goroutines open at start: %d", runtime.NumGoroutine()) + // Connect to Tor + var acn connectivity.ACN + acn, _ = connectivity.StartTor("./", "") + acn.WaitTillBootstrapped() + + // Generate Server Key + sid, sk := primitives.InitializeEphemeralIdentity() + + tokenService := privacypass.NewTokenServer() + serverAuditableStore := new(primitives.AuditableStore) + serverAuditableStore.Init(sid) + + clientAuditableStore := new(primitives.AuditableStore) + clientAuditableStore.Init(sid) + + // Init the Server running the Simple App. + var service tapir.Service + service = new(tor.BaseOnionService) + service.Init(acn, sk, &sid) + + // Goroutine Management + sg := new(sync.WaitGroup) + sg.Add(1) + go func() { + service.Listen(NewTokenBoardServer(&tokenService, serverAuditableStore)) + sg.Done() + }() + + time.Sleep(time.Second * 30) + id, sk := primitives.InitializeEphemeralIdentity() + var client tapir.Service + client = new(tor.BaseOnionService) + client.Init(acn, sk, &id) + client.Connect(sid.Hostname(), NewTokenBoardClient(clientAuditableStore, Handler{Store: clientAuditableStore}, &FreePaymentHandler{TokenService: &tokenService})) + client.WaitForCapabilityOrClose(sid.Hostname(), AuthCapability) + conn, _ := client.GetConnection(sid.Hostname()) + tba, _ := conn.App().(*TokenBoardApp) + tba.PurchaseTokens(10) + tba.Post([]byte("HELLO 1")) + tba.Post([]byte("HELLO 2")) + tba.Post([]byte("HELLO 3")) + tba.Post([]byte("HELLO 4")) + tba.Post([]byte("HELLO 5")) + + tba.Replay() + tba.Post([]byte("HELLO 6")) + tba.Post([]byte("HELLO 7")) + tba.Post([]byte("HELLO 8")) + tba.Post([]byte("HELLO 9")) + tba.Post([]byte("HELLO 10")) + + tba.Replay() + + if tba.Post([]byte("HELLO 11")) { + t.Errorf("Post should have failed.") + } + + time.Sleep(time.Second * 60) + +} diff --git a/primitives/auditablestore.go b/primitives/auditablestore.go index df839f6..bd33593 100644 --- a/primitives/auditablestore.go +++ b/primitives/auditablestore.go @@ -1,22 +1,25 @@ package primitives import ( - "crypto/subtle" "cwtch.im/tapir/primitives/core" "encoding/base64" "errors" "golang.org/x/crypto/ed25519" + "sync" ) // SignedProof encapsulates a signed proof -type SignedProof []byte +type SignedProof struct { + Commit []byte + Proof []byte +} // Message encapsulates a message for more readable code. type Message []byte // State defines an array of messages. type State struct { - message []Message + Messages []Message } // AuditableStore defines a cryptographically secure & auditable transcript of messages sent from multiple @@ -25,50 +28,64 @@ type AuditableStore struct { state State identity Identity transcript *core.Transcript - latestCommit []byte - commits map[string]bool + LatestCommit []byte + commits map[string]int + mutex sync.Mutex } // Init initializes an auditable store func (as *AuditableStore) Init(identity Identity) { as.identity = identity as.transcript = core.NewTranscript("auditable-data-store") - as.commits = make(map[string]bool) + as.commits = make(map[string]int) } // Add adds a message to the auditable store -func (as *AuditableStore) Add(message Message, latestCommit []byte) ([]byte, SignedProof, error) { - if subtle.ConstantTimeCompare(latestCommit, as.latestCommit) == 1 { - as.state.message = append(as.state.message, message) - as.transcript.AddToTranscript("new-message", message) - as.latestCommit = as.identity.Sign(as.transcript.CommitToTranscript("commit")) - return as.latestCommit, as.identity.Sign(as.latestCommit), nil - } - // this prevents multiple clients updating at the same time and will likely cause retry storms. - return nil, nil, errors.New("attempt to append out of date transcript") +func (as *AuditableStore) Add(message Message) SignedProof { + as.mutex.Lock() + defer as.mutex.Unlock() + 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)} } // GetState returns the current auditable state -func (as *AuditableStore) GetState() (State, []byte, SignedProof) { - return as.state, as.latestCommit, as.identity.Sign(as.latestCommit) +func (as *AuditableStore) GetState() (State, SignedProof) { + as.mutex.Lock() + defer as.mutex.Unlock() + return as.state, SignedProof{as.LatestCommit, as.identity.Sign(as.LatestCommit)} +} + +// GetMessagesAfter provides access to messages after the given commit. +func (as *AuditableStore) GetMessagesAfter(latestCommit []byte) []Message { + as.mutex.Lock() + defer as.mutex.Unlock() + index, ok := as.commits[base64.StdEncoding.EncodeToString(latestCommit)] + if !ok && len(latestCommit) == 32 { + return []Message{} + } else if len(latestCommit) == 0 { + index = -1 + } + return as.state.Messages[index+1:] } // MergeState merges a given state onto our state, first verifying that the two transcripts align -func (as *AuditableStore) MergeState(state State, signedStateProof SignedProof, key ed25519.PublicKey) error { - next := len(as.state.message) - for _, m := range state.message[next:] { - as.state.message = append(as.state.message, m) +func (as *AuditableStore) MergeState(state State, signedStateProof SignedProof) error { + next := len(as.state.Messages) + for i, m := range state.Messages[next:] { + 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.commits[base64.StdEncoding.EncodeToString(as.latestCommit)] = true + as.LatestCommit = as.identity.Sign(as.transcript.CommitToTranscript("commit")) + 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(key, as.latestCommit, signedStateProof) == false { + if ed25519.Verify(as.identity.PublicKey(), as.LatestCommit, signedStateProof.Proof) == false { return errors.New("state is not consistent, the server is malicious") } return nil @@ -84,16 +101,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(commit []byte, signedFraudProof SignedProof, key ed25519.PublicKey) (bool, error) { +func (as *AuditableStore) VerifyFraudProof(signedFraudProof SignedProof, key ed25519.PublicKey) (bool, error) { - if ed25519.Verify(key, commit, signedFraudProof) == false { + if ed25519.Verify(key, signedFraudProof.Commit, signedFraudProof.Proof) == 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(commit)] + _, exists := as.commits[base64.StdEncoding.EncodeToString(signedFraudProof.Commit)] 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. diff --git a/primitives/auditablestore_test.go b/primitives/auditablestore_test.go index de24f47..2ffe990 100644 --- a/primitives/auditablestore_test.go +++ b/primitives/auditablestore_test.go @@ -12,22 +12,22 @@ func TestAuditableStore(t *testing.T) { as.Init(serverID) vs.Init(serverID) // This doesn't do anything - as.Add([]byte("Hello World"), as.latestCommit) - state, _, proof := as.GetState() + as.Add([]byte("Hello World")) + state, proof := as.GetState() - if vs.MergeState(state, proof, serverID.PublicKey()) != nil { + if vs.MergeState(state, proof) != nil { t.Fatalf("Fraud Proof Failed on Honest Proof") } - commit, fraudProof, _ := as.Add([]byte("Hello World 2"), as.latestCommit) + 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, serverID.PublicKey()) != nil { + state, proof = as.GetState() + if vs.MergeState(state, proof) != nil { t.Fatalf("Fraud Proof Failed on Honest Proof") } - fraud, err := vs.VerifyFraudProof(commit, fraudProof, serverID.PublicKey()) + fraud, err := vs.VerifyFraudProof(fraudProof, serverID.PublicKey()) if err != nil { t.Fatalf("Error validated fraud proof: %v", err) diff --git a/primitives/privacypass/token.go b/primitives/privacypass/token.go index 0935490..c9d6bd6 100644 --- a/primitives/privacypass/token.go +++ b/primitives/privacypass/token.go @@ -5,6 +5,7 @@ import ( "crypto/rand" "cwtch.im/tapir/primitives/core" "fmt" + "git.openprivacy.ca/openprivacy/libricochet-go/log" "github.com/bwesterb/go-ristretto" "golang.org/x/crypto/sha3" ) @@ -29,10 +30,16 @@ type SignedToken struct { // SpentToken encapsulates the parameters needed to spend a Token type SpentToken struct { - t []byte + T []byte MAC []byte } +// TokenPaymentHandler defines an interface with external payment processors +type TokenPaymentHandler interface { + MakePayment(int) + NextToken(data []byte) (SpentToken, error) +} + // GenBlindedToken initializes the Token // GenToken() & Blind() func (t *Token) GenBlindedToken() BlindedToken { @@ -41,6 +48,7 @@ func (t *Token) GenBlindedToken() BlindedToken { t.r = new(ristretto.Scalar).Rand() Ht := sha3.Sum256(t.t) + log.Debugf("token: %x", Ht) T := new(ristretto.Point).SetElligator(&Ht) P := new(ristretto.Point).ScalarMult(T, t.r) return BlindedToken{P} diff --git a/primitives/privacypass/token_test.go b/primitives/privacypass/token_test.go index 5e3d929..31b69a1 100644 --- a/primitives/privacypass/token_test.go +++ b/primitives/privacypass/token_test.go @@ -2,6 +2,7 @@ package privacypass import ( "cwtch.im/tapir/primitives/core" + "git.openprivacy.ca/openprivacy/libricochet-go/log" "testing" ) @@ -30,6 +31,7 @@ func TestToken_SpendToken(t *testing.T) { } func TestGenerateBlindedTokenBatch(t *testing.T) { + log.SetLevel(log.LevelDebug) server := NewTokenServer() clientTranscript := core.NewTranscript("privacyPass") diff --git a/primitives/privacypass/tokenserver.go b/primitives/privacypass/tokenserver.go index 48a9740..e4d1354 100644 --- a/primitives/privacypass/tokenserver.go +++ b/primitives/privacypass/tokenserver.go @@ -5,21 +5,24 @@ import ( "cwtch.im/tapir/primitives/core" "encoding/hex" "fmt" + "git.openprivacy.ca/openprivacy/libricochet-go/log" "github.com/bwesterb/go-ristretto" "golang.org/x/crypto/sha3" + "sync" ) // TokenServer implements a token server. type TokenServer struct { - k *ristretto.Scalar - Y *ristretto.Point - seen map[string]bool + k *ristretto.Scalar + Y *ristretto.Point + seen map[string]bool + mutex sync.Mutex } // 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)} + return TokenServer{k, new(ristretto.Point).ScalarMultBase(k), make(map[string]bool), sync.Mutex{}} } // SignBlindedToken calculates kP for the given BlindedToken P @@ -57,18 +60,23 @@ func (ts *TokenServer) constructBatchProof(blindedTokens []BlindedToken, signedT // IsValid returns true a SpentToken is valid and has never been spent before, false otherwise. func (ts *TokenServer) IsValid(token SpentToken, data []byte) bool { - if _, spent := ts.seen[hex.EncodeToString(token.t)]; spent { + log.Debugf("data: [%s]", data) + ts.mutex.Lock() + defer ts.mutex.Unlock() // We only want 1 client at a time redeeming tokens to prevent double-spends + if _, spent := ts.seen[hex.EncodeToString(token.T)]; spent { return false } - Ht := sha3.Sum256(token.t) + 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()...)) + key := sha3.Sum256(append(token.T, W.Bytes()...)) mac := hmac.New(sha3.New512, key[:]) K := mac.Sum(data) + log.Debugf("mac: \n%x\nK:%x\n", token.MAC, K) result := hmac.Equal(token.MAC, K) if result == true { - ts.seen[hex.EncodeToString(token.t)] = true + ts.seen[hex.EncodeToString(token.T)] = true } return result } From ff7a32722dc90c500646b0b6fb41c3edabe59557 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Sun, 15 Sep 2019 14:20:05 -0700 Subject: [PATCH 3/8] Move to ristretto255 lib --- .gitignore | 2 + application.go | 12 ++ applications/application_chain.go | 58 ++++++ applications/auth.go | 14 +- applications/auth_test.go | 8 +- applications/proof_of_work_app.go | 102 ++++++++++ applications/token_app.go | 58 ++++++ applications/tokenboard.go | 192 ------------------ applications/tokenboard/client.go | 112 ++++++++++ applications/tokenboard/common.go | 52 +++++ applications/tokenboard/server.go | 90 ++++++++ .../tokenboard/tokenboard_integration_test.go | 151 ++++++++++++++ applications/tokenboard_integration_test.go | 118 ----------- applications/transcript_app.go | 7 +- go.mod | 6 + go.sum | 13 +- networks/tor/BaseOnionService.go | 2 +- persistence/bolt_persistence.go | 76 +++++++ persistence/bolt_persistence_test.go | 23 +++ persistence/persistence.go | 11 + primitives/{ => auditable}/auditablestore.go | 116 ++++++++--- primitives/auditable/auditablestore_test.go | 70 +++++++ primitives/auditablestore_test.go | 39 ---- primitives/core/operations.go | 186 +++++++++++++++++ primitives/core/transcript.go | 69 ++++--- primitives/dlog.go | 71 ------- primitives/privacypass/common.go | 17 ++ primitives/privacypass/dlogeq.go | 58 +++--- primitives/privacypass/token.go | 50 +++-- primitives/privacypass/token_test.go | 22 +- primitives/privacypass/tokenserver.go | 100 ++++++--- service.go | 28 ++- testing/tests.sh | 3 + 33 files changed, 1355 insertions(+), 581 deletions(-) create mode 100644 applications/application_chain.go create mode 100644 applications/proof_of_work_app.go create mode 100644 applications/token_app.go delete mode 100644 applications/tokenboard.go create mode 100644 applications/tokenboard/client.go create mode 100644 applications/tokenboard/common.go create mode 100644 applications/tokenboard/server.go create mode 100644 applications/tokenboard/tokenboard_integration_test.go delete mode 100644 applications/tokenboard_integration_test.go create mode 100644 persistence/bolt_persistence.go create mode 100644 persistence/bolt_persistence_test.go create mode 100644 persistence/persistence.go rename primitives/{ => auditable}/auditablestore.go (51%) create mode 100644 primitives/auditable/auditablestore_test.go delete mode 100644 primitives/auditablestore_test.go create mode 100644 primitives/core/operations.go delete mode 100644 primitives/dlog.go create mode 100644 primitives/privacypass/common.go diff --git a/.gitignore b/.gitignore index 003fb79..99c6321 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ vendor/ coverage.out /testing/tor/ /applications/tor/ +*.db +/applications/tokenboard/tor/ diff --git a/application.go b/application.go index 994e78b..1fd47b9 100644 --- a/application.go +++ b/application.go @@ -4,9 +4,21 @@ import ( "cwtch.im/tapir/primitives/core" ) +// Capability defines a status granted to a connection, from an application. That allows the connection to access +// other Application or functions within an Application. +type Capability string + // Application defines the interface for all Tapir Applications type Application interface { NewInstance() Application Init(connection Connection) Transcript() *core.Transcript + PropagateTranscript(transcript *core.Transcript) +} + +// InteractiveApplication defines the interface for interactive Tapir applications (apps that expect the user to send +// and receive messages from) +type InteractiveApplication interface { + Application + Listen() } diff --git a/applications/application_chain.go b/applications/application_chain.go new file mode 100644 index 0000000..3b440af --- /dev/null +++ b/applications/application_chain.go @@ -0,0 +1,58 @@ +package applications + +import ( + "cwtch.im/tapir" +) + +// ApplicationChain is a meta-app that can be used to build complex applications from other applications +type ApplicationChain struct { + TranscriptApp + apps []tapir.Application + endapp tapir.InteractiveApplication + capabilities []tapir.Capability +} + +// ChainApplication adds a new application to the chain. Returns a pointer to app so this call +// can itself be chained. +func (appchain *ApplicationChain) ChainApplication(app tapir.Application, capability tapir.Capability) *ApplicationChain { + appchain.apps = append(appchain.apps, app.NewInstance()) + appchain.capabilities = append(appchain.capabilities, capability) + return appchain +} + +// ChainInteractiveApplication adds an interactive application to the chain. There can only be 1 interactive application. +func (appchain *ApplicationChain) ChainInteractiveApplication(app tapir.InteractiveApplication) *ApplicationChain { + appchain.endapp = app + return appchain +} + +// NewInstance should always return a new instantiation of the application. +func (appchain *ApplicationChain) NewInstance() tapir.Application { + applicationChain := new(ApplicationChain) + for _, app := range appchain.apps { + applicationChain.apps = append(applicationChain.apps, app.NewInstance()) + } + applicationChain.capabilities = appchain.capabilities + return applicationChain +} + +// Init is run when the connection is first started. +func (appchain *ApplicationChain) Init(connection tapir.Connection) { + appchain.TranscriptApp.Init(connection) + for i, app := range appchain.apps { + app.PropagateTranscript(appchain.transcript) + app.Init(connection) + if connection.HasCapability(appchain.capabilities[i]) == false { + connection.Close() + return + } + connection.SetApp(app) + } +} + +// Listen calls listen on the Interactive application +func (appchain *ApplicationChain) Listen() { + if appchain.endapp != nil { + appchain.endapp.Listen() + } +} diff --git a/applications/auth.go b/applications/auth.go index e9da438..0a57176 100644 --- a/applications/auth.go +++ b/applications/auth.go @@ -17,7 +17,7 @@ type AuthMessage struct { } // AuthCapability defines the Authentication Capability granted by AuthApp -const AuthCapability = "AUTH" +const AuthCapability = tapir.Capability("AuthenticationCapability") // AuthApp is the concrete Application type that handles Authentication type AuthApp struct { @@ -83,7 +83,9 @@ func (ea *AuthApp) Init(connection tapir.Connection) { // Derive a challenge from the transcript of the public parameters of this authentication protocol transcript := ea.Transcript() - transcript.AddToTranscript("auth-protocol", []byte(outboundHostname+"-"+inboundHostname)) + transcript.NewProtocol("auth-app") + transcript.AddToTranscript("outbound-hostname", []byte(outboundHostname)) + transcript.AddToTranscript("inbound-hostname", []byte(inboundHostname)) transcript.AddToTranscript("outbound-challenge", outboundAuthMessage) transcript.AddToTranscript("inbound-challenge", inboundAuthMessage) challengeBytes := transcript.CommitToTranscript("3dh-auth-challenge") @@ -94,14 +96,16 @@ func (ea *AuthApp) Init(connection tapir.Connection) { // Since we have set the encryption key on the connection the connection will encrypt any messages we send with that key // To test that the remote peer has done the same we calculate a challenge hash based on the transcript so far and send it to them + // along with our hostname // We expect the remote to do the same, and compare the two. // If successful we extend our auth capability to the connection and reassert the hostname. // We note that the only successful scenario here requires that the remote peer have successfully derived the same // encryption key and the same transcript challenge. - connection.Send(challengeBytes) + connection.Send(append(challengeBytes, []byte(connection.ID().Hostname())...)) remoteChallenge := connection.Expect() - if subtle.ConstantTimeCompare(challengeBytes, remoteChallenge) == 1 { - connection.SetHostname(utils.GetTorV3Hostname(remoteAuthMessage.LongTermPublicKey)) + assertedHostname := utils.GetTorV3Hostname(remoteAuthMessage.LongTermPublicKey) + if subtle.ConstantTimeCompare(append(challengeBytes, []byte(assertedHostname)...), remoteChallenge) == 1 { + connection.SetHostname(assertedHostname) connection.SetCapability(AuthCapability) } else { log.Errorf("Failed Decrypt Challenge: [%x] [%x]\n", remoteChallenge, challengeBytes) diff --git a/applications/auth_test.go b/applications/auth_test.go index da1888a..bd5129a 100644 --- a/applications/auth_test.go +++ b/applications/auth_test.go @@ -47,11 +47,11 @@ func (MockConnection) SetHostname(hostname string) { panic("implement me") } -func (MockConnection) HasCapability(name string) bool { +func (MockConnection) HasCapability(name tapir.Capability) bool { panic("implement me") } -func (MockConnection) SetCapability(name string) { +func (MockConnection) SetCapability(name tapir.Capability) { panic("implement me") } @@ -72,6 +72,10 @@ func (MockConnection) App() tapir.Application { return nil } +func (MockConnection) SetApp(tapir.Application) { + // no op +} + func (MockConnection) IsClosed() bool { panic("implement me") } diff --git a/applications/proof_of_work_app.go b/applications/proof_of_work_app.go new file mode 100644 index 0000000..697d78f --- /dev/null +++ b/applications/proof_of_work_app.go @@ -0,0 +1,102 @@ +package applications + +import ( + "crypto/sha256" + "cwtch.im/tapir" + "cwtch.im/tapir/primitives/core" + "git.openprivacy.ca/openprivacy/libricochet-go/log" +) + +// ProofOfWorkApplication forces the incoming connection to do proof of work before granting a capability +type ProofOfWorkApplication struct { + TranscriptApp +} + +// transcript constants +const ( + PoWApp = "pow-app" + PoWSeed = "pow-seed" + PoWChallenge = "pow-challenge" + PoWPRNG = "pow-prng" + PoWSolution = "pow-solution" +) + +// SuccessfulProofOfWorkCapability is given when a successfully PoW Challenge has been Completed +const SuccessfulProofOfWorkCapability = tapir.Capability("SuccessfulProofOfWorkCapability") + +// NewInstance should always return a new instantiation of the application. +func (powapp *ProofOfWorkApplication) NewInstance() tapir.Application { + return new(ProofOfWorkApplication) +} + +// Init is run when the connection is first started. +func (powapp *ProofOfWorkApplication) Init(connection tapir.Connection) { + powapp.Transcript().NewProtocol(PoWApp) + if connection.IsOutbound() { + powapp.Transcript().AddToTranscript(PoWSeed, connection.Expect()) + solution := powapp.solveChallenge(powapp.Transcript().CommitToTranscript(PoWChallenge), powapp.transcript.CommitToPRNG(PoWPRNG)) + powapp.transcript.AddToTranscript(PoWSolution, solution) + connection.Send(solution) + connection.SetCapability(SuccessfulProofOfWorkCapability) // We can self grant.because the server will close the connection on failure + return + } + + // We may be the first application, in which case we need to randomize the transcript challenge + // We use the random hostname of the inbound server (if we've authenticated them then the challenge will + // already be sufficiently randomized, so this doesn't hurt) + // It does sadly mean an additional round trip. + powapp.Transcript().AddToTranscript(PoWSeed, []byte(connection.Hostname())) + connection.Send([]byte(connection.Hostname())) + solution := connection.Expect() + challenge := powapp.Transcript().CommitToTranscript(PoWChallenge) + // soft-commitment to the prng, doesn't force the client to use it (but we could technically check that it did, not necessary for the security of this App) + powapp.transcript.CommitToPRNG(PoWPRNG) + powapp.transcript.AddToTranscript(PoWSolution, solution) + if powapp.validateChallenge(challenge, solution) { + connection.SetCapability(SuccessfulProofOfWorkCapability) + return + } +} + +// SolveChallenge takes in a challenge and a message and returns a solution +// The solution is a 24 byte nonce which when hashed with the challenge and the message +// produces a sha256 hash with Difficulty leading 0s +func (powapp *ProofOfWorkApplication) solveChallenge(challenge []byte, prng core.PRNG) []byte { + solved := false + var sum [32]byte + solution := []byte{} + solve := make([]byte, len(challenge)+32) + for !solved { + + solution = prng.Next().Encode(nil) + + copy(solve[0:], solution[:]) + copy(solve[len(solution):], challenge[:]) + sum = sha256.Sum256(solve) + + solved = true + for i := 0; i < 2; i++ { + if sum[i] != 0x00 { + solved = false + } + } + } + log.Debugf("Validated Challenge %v: %v %v\n", challenge, solution, sum) + return solution[:] +} + +// ValidateChallenge returns true if the message and spamguard pass the challenge +func (powapp *ProofOfWorkApplication) validateChallenge(challenge []byte, solution []byte) bool { + solve := make([]byte, len(challenge)+32) + copy(solve[0:], solution[0:32]) + copy(solve[32:], challenge[:]) + sum := sha256.Sum256(solve) + + for i := 0; i < 2; i++ { + if sum[i] != 0x00 { + return false + } + } + log.Debugf("Validated Challenge %v: %v %v\n", challenge, solution, sum) + return true +} diff --git a/applications/token_app.go b/applications/token_app.go new file mode 100644 index 0000000..1bec236 --- /dev/null +++ b/applications/token_app.go @@ -0,0 +1,58 @@ +package applications + +import ( + "cwtch.im/tapir" + "cwtch.im/tapir/primitives/privacypass" + "encoding/json" + "git.openprivacy.ca/openprivacy/libricochet-go/log" +) + +// TokenApplication provides Tokens for PoW +type TokenApplication struct { + TranscriptApp + TokenService *privacypass.TokenServer + Tokens []*privacypass.Token +} + +// HasTokensCapability is granted once the client has obtained signed tokens +const HasTokensCapability = tapir.Capability("HasTokensCapability") + +// NewInstance should always return a new instantiation of the application. +func (powapp *TokenApplication) NewInstance() tapir.Application { + app := new(TokenApplication) + app.TokenService = powapp.TokenService + return app +} + +// Init is run when the connection is first started. +func (powapp *TokenApplication) Init(connection tapir.Connection) { + powapp.Transcript().NewProtocol("token-app") + log.Debugf(powapp.Transcript().OutputTranscriptToAudit()) + if connection.IsOutbound() { + tokens, blinded := privacypass.GenerateBlindedTokenBatch(10) + data, _ := json.Marshal(blinded) + connection.Send(data) + var signedBatch privacypass.SignedBatchWithProof + err := json.Unmarshal(connection.Expect(), &signedBatch) + if err == nil { + verified := privacypass.UnblindSignedTokenBatch(tokens, blinded, signedBatch.SignedTokens, powapp.TokenService.Y, signedBatch.Proof, powapp.Transcript()) + if verified { + log.Debugf("Successfully obtained signed tokens") + powapp.Tokens = tokens + connection.SetCapability(HasTokensCapability) + return + } + log.Debugf("Failed to verify signed token batch") + } + } else { + var blinded []privacypass.BlindedToken + err := json.Unmarshal(connection.Expect(), &blinded) + if err == nil { + batchProof := powapp.TokenService.SignBlindedTokenBatch(blinded, powapp.Transcript()) + log.Debugf(powapp.Transcript().OutputTranscriptToAudit()) + data, _ := json.Marshal(batchProof) + connection.Send(data) + return + } + } +} diff --git a/applications/tokenboard.go b/applications/tokenboard.go deleted file mode 100644 index d471e8a..0000000 --- a/applications/tokenboard.go +++ /dev/null @@ -1,192 +0,0 @@ -package applications - -import ( - "cwtch.im/tapir" - "cwtch.im/tapir/primitives" - "cwtch.im/tapir/primitives/privacypass" - "encoding/json" - "git.openprivacy.ca/openprivacy/libricochet-go/log" -) - -// TokenBoardApp defines a Tapir Meta=App which provides a global cryptographic transcript -type TokenBoardApp struct { - AuthApp - connection tapir.Connection - TokenService *privacypass.TokenServer - AuditableStore *primitives.AuditableStore - paymentHandler privacypass.TokenPaymentHandler - handler TokenBoardAppHandler -} - -// TokenBoardAppHandler allows clients to react to specific events. -type TokenBoardAppHandler interface { - HandleNewMessages(previousLastCommit []byte) -} - -// NewTokenBoardClient generates a new Client for Token Board -func NewTokenBoardClient(store *primitives.AuditableStore, handler TokenBoardAppHandler, paymentHandler privacypass.TokenPaymentHandler) tapir.Application { - tba := new(TokenBoardApp) - tba.TokenService = nil - tba.AuditableStore = store - tba.handler = handler - tba.paymentHandler = paymentHandler - return tba -} - -// NewTokenBoardServer generates new Server for Token Board -func NewTokenBoardServer(tokenService *privacypass.TokenServer, store *primitives.AuditableStore) tapir.Application { - tba := new(TokenBoardApp) - tba.TokenService = tokenService - tba.AuditableStore = store - return tba -} - -// NewInstance creates a new TokenBoardApp -func (ta *TokenBoardApp) NewInstance() tapir.Application { - tba := new(TokenBoardApp) - tba.TokenService = ta.TokenService - tba.AuditableStore = ta.AuditableStore - tba.handler = ta.handler - tba.paymentHandler = ta.paymentHandler - return tba -} - -// Init initializes the cryptographic TokenBoardApp -func (ta *TokenBoardApp) Init(connection tapir.Connection) { - ta.AuthApp.Init(connection) - - if connection.HasCapability(AuthCapability) { - ta.connection = connection - // If we are a server, now we can start listening for inbound messages - if ta.connection.IsOutbound() { - go ta.listen(ta.clientSwitch) - } else { - go ta.listen(ta.serverSwitch) - } - return - } - connection.Close() -} - -// TokenBoardMessage encapsulates the application protocol -type TokenBoardMessage struct { - MessageType string - PostRequest PostRequest `json:",omitempty"` - PostResult PostResult `json:",omitempty"` - ReplayRequest ReplayRequest `json:",omitempty"` - ReplayResponse ReplayResponse `json:",omitempty"` -} - -// ReplayRequest requests a reply from the given Commit -type ReplayRequest struct { - LastCommit []byte -} - -// PostRequest requests to post the message to the board with the given token -type PostRequest struct { - Token privacypass.SpentToken - Message primitives.Message -} - -// PostResult returns the success of a given post attempt -type PostResult struct { - Success bool - Proof primitives.SignedProof -} - -// ReplayResponse is sent by the server before a stream of replayed messages -type ReplayResponse struct { - NumMessages int -} - -func (ta *TokenBoardApp) clientSwitch(message TokenBoardMessage) { - switch message.MessageType { - case "PostResult": - log.Debugf("Post result: %x", message.PostResult.Proof) - case "ReplayResponse": - var state primitives.State - log.Debugf("Replaying %v Messages...", message.ReplayResponse.NumMessages) - lastCommit := ta.AuditableStore.LatestCommit - for i := 0; i < message.ReplayResponse.NumMessages; i++ { - message := ta.connection.Expect() - state.Messages = append(state.Messages, message) - } - data := ta.connection.Expect() - var signedProof primitives.SignedProof - json.Unmarshal(data, &signedProof) - err := ta.AuditableStore.MergeState(state, signedProof) - if err == nil { - log.Debugf("Successfully updated Auditable Store") - ta.handler.HandleNewMessages(lastCommit) - } else { - log.Debugf("Error updating Auditable Store %v", err) - } - } -} - -func (ta *TokenBoardApp) serverSwitch(message TokenBoardMessage) { - switch message.MessageType { - case "PostRequest": - postrequest := message.PostRequest - log.Debugf("Received a Post Message Request: %x %x", postrequest.Token, postrequest.Message) - ta.postMessageRequest(postrequest.Token, postrequest.Message) - case "ReplayRequest": - state, proof := ta.AuditableStore.GetState() - response, _ := json.Marshal(TokenBoardMessage{MessageType: "ReplayResponse", ReplayResponse: ReplayResponse{len(state.Messages)}}) - ta.connection.Send(response) - for _, message := range state.Messages { - ta.connection.Send(message) - } - data, _ := json.Marshal(proof) - ta.connection.Send(data) - } -} - -func (ta *TokenBoardApp) listen(switchFn func(TokenBoardMessage)) { - for { - data := ta.connection.Expect() - if len(data) == 0 { - return // connection is closed - } - - var message TokenBoardMessage - json.Unmarshal(data, &message) - log.Debugf("Received a Message: %v", message) - switchFn(message) - } -} - -// Replay posts a Replay Message to the server. -func (ta *TokenBoardApp) Replay() { - data, _ := json.Marshal(TokenBoardMessage{MessageType: "ReplayRequest"}) - ta.connection.Send(data) -} - -// PurchaseTokens purchases the given number of tokens from the server (using the provided payment handler) -func (ta *TokenBoardApp) PurchaseTokens(num int) { - ta.paymentHandler.MakePayment(num) -} - -// Post sends a Post Request to the server -func (ta *TokenBoardApp) Post(message primitives.Message) bool { - token, err := ta.paymentHandler.NextToken(message) - if err == nil { - data, _ := json.Marshal(TokenBoardMessage{MessageType: "PostRequest", PostRequest: PostRequest{Token: token, Message: message}}) - ta.connection.Send(data) - return true - } - return false -} - -func (ta *TokenBoardApp) postMessageRequest(token privacypass.SpentToken, message primitives.Message) { - if ta.TokenService.IsValid(token, message) { - log.Debugf("Token is valid") - signedproof := ta.AuditableStore.Add(message) - data, _ := json.Marshal(TokenBoardMessage{MessageType: "PostResult", PostResult: PostResult{true, signedproof}}) - ta.connection.Send(data) - } else { - log.Debugf("Attempt to spend an invalid token") - data, _ := json.Marshal(TokenBoardMessage{MessageType: "PostResult", PostResult: PostResult{false, primitives.SignedProof{}}}) - ta.connection.Send(data) - } -} diff --git a/applications/tokenboard/client.go b/applications/tokenboard/client.go new file mode 100644 index 0000000..55fa387 --- /dev/null +++ b/applications/tokenboard/client.go @@ -0,0 +1,112 @@ +package tokenboard + +import ( + "cwtch.im/tapir" + "cwtch.im/tapir/applications" + "cwtch.im/tapir/primitives/auditable" + "cwtch.im/tapir/primitives/privacypass" + "encoding/json" + "git.openprivacy.ca/openprivacy/libricochet-go/log" +) + +// NewTokenBoardClient generates a new Client for Token Board +func NewTokenBoardClient(store *auditable.Store, handler AppHandler, paymentHandler privacypass.TokenPaymentHandler) tapir.Application { + tba := new(Client) + tba.AuditableStore = store + tba.handler = handler + tba.paymentHandler = paymentHandler + return tba +} + +// Client defines a client for the TokenBoard server +type Client struct { + applications.AuthApp + connection tapir.Connection + AuditableStore *auditable.Store + paymentHandler privacypass.TokenPaymentHandler + handler AppHandler +} + +// NewInstance Client a new TokenBoardApp +func (ta *Client) NewInstance() tapir.Application { + tba := new(Client) + tba.AuditableStore = ta.AuditableStore + tba.handler = ta.handler + tba.paymentHandler = ta.paymentHandler + return tba +} + +// Init initializes the cryptographic TokenBoardApp +func (ta *Client) Init(connection tapir.Connection) { + ta.AuthApp.Init(connection) + + if connection.HasCapability(applications.AuthCapability) { + ta.connection = connection + go ta.Listen() + return + } + connection.Close() +} + +// Listen processes the messages for this application +func (ta *Client) Listen() { + for { + log.Debugf("Client waiting...") + data := ta.connection.Expect() + if len(data) == 0 { + log.Debugf("Server closed the connection...") + return // connection is closed + } + + var message Message + json.Unmarshal(data, &message) + log.Debugf("Received a Message: %v", message) + switch message.MessageType { + case postResultMessage: + log.Debugf("Post result: %x", message.PostResult.Proof) + case replayResultMessage: + var state auditable.State + log.Debugf("Replaying %v Messages...", message.ReplayResult.NumMessages) + lastCommit := ta.AuditableStore.LatestCommit + for i := 0; i < message.ReplayResult.NumMessages; i++ { + message := ta.connection.Expect() + state.Messages = append(state.Messages, message) + } + data := ta.connection.Expect() + var signedProof auditable.SignedProof + json.Unmarshal(data, &signedProof) + state.SignedProof = signedProof + err := ta.AuditableStore.AppendState(state) + if err == nil { + log.Debugf("Successfully updated Auditable Store %v", ta.AuditableStore.LatestCommit) + ta.handler.HandleNewMessages(lastCommit) + } else { + log.Debugf("Error updating Auditable Store %v", err) + } + } + } +} + +// Replay posts a Replay Message to the server. +func (ta *Client) Replay() { + log.Debugf("Sending replay request for %v", ta.AuditableStore.LatestCommit) + data, _ := json.Marshal(Message{MessageType: replayRequestMessage, ReplayRequest: replayRequest{LastCommit: ta.AuditableStore.LatestCommit}}) + ta.connection.Send(data) +} + +// PurchaseTokens purchases the given number of tokens from the server (using the provided payment handler) +func (ta *Client) PurchaseTokens() { + ta.paymentHandler.MakePayment() +} + +// Post sends a Post Request to the server +func (ta *Client) Post(message auditable.Message) bool { + token, err := ta.paymentHandler.NextToken(message) + if err == nil { + data, _ := json.Marshal(Message{MessageType: postRequestMessage, PostRequest: postRequest{Token: token, Message: message}}) + ta.connection.Send(data) + return true + } + log.Debugf("No Valid Tokens: %v", err) + return false +} diff --git a/applications/tokenboard/common.go b/applications/tokenboard/common.go new file mode 100644 index 0000000..ba746e1 --- /dev/null +++ b/applications/tokenboard/common.go @@ -0,0 +1,52 @@ +package tokenboard + +import ( + "cwtch.im/tapir/primitives/auditable" + "cwtch.im/tapir/primitives/privacypass" +) + +// AppHandler allows clients to react to specific events. +type AppHandler interface { + HandleNewMessages(previousLastCommit []byte) +} + +// MessageType defines the enum for TokenBoard messages +type messageType int + +const ( + replayRequestMessage messageType = iota + replayResultMessage + postRequestMessage + postResultMessage +) + +// Message encapsulates the application protocol +type Message struct { + MessageType messageType + PostRequest postRequest `json:",omitempty"` + PostResult postResult `json:",omitempty"` + ReplayRequest replayRequest `json:",omitempty"` + ReplayResult replayResult `json:",omitempty"` +} + +// ReplayRequest requests a reply from the given Commit +type replayRequest struct { + LastCommit []byte +} + +// PostRequest requests to post the message to the board with the given token +type postRequest struct { + Token privacypass.SpentToken + Message auditable.Message +} + +// PostResult returns the success of a given post attempt +type postResult struct { + Success bool + Proof auditable.SignedProof +} + +// ReplayResult is sent by the server before a stream of replayed messages +type replayResult struct { + NumMessages int +} diff --git a/applications/tokenboard/server.go b/applications/tokenboard/server.go new file mode 100644 index 0000000..764f595 --- /dev/null +++ b/applications/tokenboard/server.go @@ -0,0 +1,90 @@ +package tokenboard + +import ( + "cwtch.im/tapir" + "cwtch.im/tapir/applications" + "cwtch.im/tapir/primitives/auditable" + "cwtch.im/tapir/primitives/privacypass" + "encoding/json" + "git.openprivacy.ca/openprivacy/libricochet-go/log" +) + +// NewTokenBoardServer generates new Server for Token Board +func NewTokenBoardServer(tokenService *privacypass.TokenServer, store *auditable.Store) tapir.Application { + tba := new(Server) + tba.TokenService = tokenService + tba.AuditableStore = store + return tba +} + +// Server defines the token board server +type Server struct { + applications.AuthApp + connection tapir.Connection + TokenService *privacypass.TokenServer + AuditableStore *auditable.Store +} + +// NewInstance creates a new TokenBoardApp +func (ta *Server) NewInstance() tapir.Application { + tba := new(Server) + tba.TokenService = ta.TokenService + tba.AuditableStore = ta.AuditableStore + return tba +} + +// Init initializes the cryptographic TokenBoardApp +func (ta *Server) Init(connection tapir.Connection) { + ta.AuthApp.Init(connection) + + if connection.HasCapability(applications.AuthCapability) { + ta.connection = connection + go ta.Listen() + return + } + connection.Close() +} + +// Listen processes the messages for this application +func (ta *Server) Listen() { + for { + data := ta.connection.Expect() + if len(data) == 0 { + return // connection is closed + } + + var message Message + json.Unmarshal(data, &message) + log.Debugf("Received a Message: %v", message) + switch message.MessageType { + case postRequestMessage: + postrequest := message.PostRequest + log.Debugf("Received a Post Message Request: %x %x", postrequest.Token, postrequest.Message) + ta.postMessageRequest(postrequest.Token, postrequest.Message) + case replayRequestMessage: + log.Debugf("Received Replay Request %v", message.ReplayRequest) + state := ta.AuditableStore.GetStateAfter(message.ReplayRequest.LastCommit) + response, _ := json.Marshal(Message{MessageType: replayResultMessage, ReplayResult: replayResult{len(state.Messages)}}) + log.Debugf("Sending Replay Response %v", replayResult{len(state.Messages)}) + ta.connection.Send(response) + for _, message := range state.Messages { + ta.connection.Send(message) + } + data, _ := json.Marshal(state.SignedProof) + ta.connection.Send(data) + } + } +} + +func (ta *Server) postMessageRequest(token privacypass.SpentToken, message auditable.Message) { + if err := ta.TokenService.SpendToken(token, message); err == nil { + log.Debugf("Token is valid") + signedproof := ta.AuditableStore.Add(message) + data, _ := json.Marshal(Message{MessageType: postResultMessage, PostResult: postResult{true, signedproof}}) + ta.connection.Send(data) + } else { + log.Debugf("Attempt to spend an invalid token: %v", err) + data, _ := json.Marshal(Message{MessageType: postResultMessage, PostResult: postResult{false, auditable.SignedProof{}}}) + ta.connection.Send(data) + } +} diff --git a/applications/tokenboard/tokenboard_integration_test.go b/applications/tokenboard/tokenboard_integration_test.go new file mode 100644 index 0000000..ba75cb3 --- /dev/null +++ b/applications/tokenboard/tokenboard_integration_test.go @@ -0,0 +1,151 @@ +package tokenboard + +import ( + "cwtch.im/tapir" + "cwtch.im/tapir/applications" + "cwtch.im/tapir/networks/tor" + "cwtch.im/tapir/primitives" + "cwtch.im/tapir/primitives/auditable" + "cwtch.im/tapir/primitives/privacypass" + "errors" + "git.openprivacy.ca/openprivacy/libricochet-go/connectivity" + "git.openprivacy.ca/openprivacy/libricochet-go/log" + "runtime" + "sync" + "testing" + "time" +) + +type Handler struct { + Store *auditable.Store +} + +func (h Handler) HandleNewMessages(previousLastCommit []byte) { + log.Debugf("Handling Messages After %x", previousLastCommit) + messages := h.Store.GetMessagesAfter(previousLastCommit) + for _, message := range messages { + log.Debugf("Message %s", message) + } +} + +type FreePaymentHandler struct { + tokens []*privacypass.Token + TokenService *privacypass.TokenServer + ACN connectivity.ACN + ServerHostname string +} + +func (fph *FreePaymentHandler) MakePayment() { + id, sk := primitives.InitializeEphemeralIdentity() + var client tapir.Service + client = new(tor.BaseOnionService) + client.Init(fph.ACN, sk, &id) + + tokenApplication := new(applications.TokenApplication) + tokenApplication.TokenService = fph.TokenService + powTokenApp := new(applications.ApplicationChain). + ChainApplication(new(applications.ProofOfWorkApplication), applications.SuccessfulProofOfWorkCapability). + ChainApplication(tokenApplication, applications.HasTokensCapability) + client.Connect(fph.ServerHostname, powTokenApp) + conn,err := client.WaitForCapabilityOrClose(fph.ServerHostname, applications.HasTokensCapability) + if err == nil { + powtapp, _ := conn.App().(*applications.TokenApplication) + fph.tokens = append(fph.tokens, powtapp.Tokens...) + log.Debugf("Transcript: %v", powtapp.Transcript().OutputTranscriptToAudit()) + conn.Close() + } + log.Debugf("Error making payment: %v", err) +} + +func (fph *FreePaymentHandler) NextToken(data []byte) (privacypass.SpentToken, error) { + if len(fph.tokens) == 0 { + return privacypass.SpentToken{}, errors.New("No more tokens") + } + token := fph.tokens[0] + fph.tokens = fph.tokens[1:] + return token.SpendToken(data), nil +} + +func TestTokenBoardApp(t *testing.T) { + // numRoutinesStart := runtime.NumGoroutine() + log.SetLevel(log.LevelDebug) + log.Infof("Number of goroutines open at start: %d", runtime.NumGoroutine()) + // Connect to Tor + var acn connectivity.ACN + acn, _ = connectivity.StartTor("./", "") + acn.WaitTillBootstrapped() + + // Generate Server Key + sid, sk := primitives.InitializeEphemeralIdentity() + tokenService := privacypass.NewTokenServer() + serverAuditableStore := new(auditable.Store) + serverAuditableStore.Init(sid) + + clientAuditableStore := new(auditable.Store) + // Only initialize with public parameters + sidpubk := sid.PublicKey() + publicsid := primitives.InitializeIdentity("server", nil, &sidpubk) + clientAuditableStore.Init(publicsid) + + // Init the Server running the Simple App. + var service tapir.Service + service = new(tor.BaseOnionService) + service.Init(acn, sk, &sid) + + // Goroutine Management + sg := new(sync.WaitGroup) + sg.Add(1) + go func() { + service.Listen(NewTokenBoardServer(&tokenService, serverAuditableStore)) + sg.Done() + }() + + // Init the Server running the PoW Token App. + var powTokenService tapir.Service + powTokenService = new(tor.BaseOnionService) + spowid, spowk := primitives.InitializeEphemeralIdentity() + powTokenService.Init(acn, spowk, &spowid) + sg.Add(1) + go func() { + tokenApplication := new(applications.TokenApplication) + tokenApplication.TokenService = &tokenService + powTokenApp := new(applications.ApplicationChain). + ChainApplication(new(applications.ProofOfWorkApplication), applications.SuccessfulProofOfWorkCapability). + ChainApplication(tokenApplication, applications.HasTokensCapability) + powTokenService.Listen(powTokenApp) + sg.Done() + }() + + time.Sleep(time.Second * 60) // wait for server to initialize + id, sk := primitives.InitializeEphemeralIdentity() + var client tapir.Service + client = new(tor.BaseOnionService) + client.Init(acn, sk, &id) + client.Connect(sid.Hostname(), NewTokenBoardClient(clientAuditableStore, Handler{Store: clientAuditableStore}, &FreePaymentHandler{ACN: acn, TokenService: &tokenService, ServerHostname: spowid.Hostname()})) + client.WaitForCapabilityOrClose(sid.Hostname(), applications.AuthCapability) + conn, _ := client.GetConnection(sid.Hostname()) + tba, _ := conn.App().(*Client) + tba.PurchaseTokens() + tba.Post([]byte("HELLO 1")) + tba.Post([]byte("HELLO 2")) + tba.Post([]byte("HELLO 3")) + tba.Post([]byte("HELLO 4")) + tba.Post([]byte("HELLO 5")) + tba.Replay() + time.Sleep(time.Second * 10) // We have to wait for the async replay request! + tba.Post([]byte("HELLO 6")) + tba.Post([]byte("HELLO 7")) + tba.Post([]byte("HELLO 8")) + tba.Post([]byte("HELLO 9")) + tba.Post([]byte("HELLO 10")) + tba.Replay() + time.Sleep(time.Second * 10) // We have to wait for the async replay request! + + if tba.Post([]byte("HELLO 11")) { + t.Errorf("Post should have failed.") + } + + time.Sleep(time.Second * 10) + acn.Close() + sg.Wait() +} diff --git a/applications/tokenboard_integration_test.go b/applications/tokenboard_integration_test.go deleted file mode 100644 index 285585d..0000000 --- a/applications/tokenboard_integration_test.go +++ /dev/null @@ -1,118 +0,0 @@ -package applications - -import ( - "cwtch.im/tapir" - "cwtch.im/tapir/networks/tor" - "cwtch.im/tapir/primitives" - "cwtch.im/tapir/primitives/core" - "cwtch.im/tapir/primitives/privacypass" - "errors" - "git.openprivacy.ca/openprivacy/libricochet-go/connectivity" - "git.openprivacy.ca/openprivacy/libricochet-go/log" - "runtime" - "sync" - "testing" - "time" -) - -type Handler struct { - Store *primitives.AuditableStore -} - -func (h Handler) HandleNewMessages(previousLastCommit []byte) { - log.Debugf("Handling Messages After %x", previousLastCommit) - messages := h.Store.GetMessagesAfter(previousLastCommit) - for _, message := range messages { - log.Debugf("Message %s", message) - } -} - -type FreePaymentHandler struct { - tokens []*privacypass.Token - TokenService *privacypass.TokenServer -} - -func (fph *FreePaymentHandler) MakePayment(int) { - tokens, blindedTokens := privacypass.GenerateBlindedTokenBatch(10) - // Obtained some signed tokens, in reality these would be bought and paid for through some other mechanism. - clientTranscript := core.NewTranscript("privacyPass") - serverTranscript := core.NewTranscript("privacyPass") - - signedTokens, proof := fph.TokenService.SignBlindedTokenBatch(blindedTokens, serverTranscript) - privacypass.UnblindSignedTokenBatch(tokens, blindedTokens, signedTokens, fph.TokenService.Y, proof, clientTranscript) - fph.tokens = append(fph.tokens, tokens...) -} - -func (fph *FreePaymentHandler) NextToken(data []byte) (privacypass.SpentToken, error) { - if len(fph.tokens) == 0 { - return privacypass.SpentToken{}, errors.New("No more tokens") - } - token := fph.tokens[0] - fph.tokens = fph.tokens[1:] - return token.SpendToken(data), nil -} - -func TestTokenBoardApp(t *testing.T) { - // numRoutinesStart := runtime.NumGoroutine() - log.SetLevel(log.LevelDebug) - log.Infof("Number of goroutines open at start: %d", runtime.NumGoroutine()) - // Connect to Tor - var acn connectivity.ACN - acn, _ = connectivity.StartTor("./", "") - acn.WaitTillBootstrapped() - - // Generate Server Key - sid, sk := primitives.InitializeEphemeralIdentity() - - tokenService := privacypass.NewTokenServer() - serverAuditableStore := new(primitives.AuditableStore) - serverAuditableStore.Init(sid) - - clientAuditableStore := new(primitives.AuditableStore) - clientAuditableStore.Init(sid) - - // Init the Server running the Simple App. - var service tapir.Service - service = new(tor.BaseOnionService) - service.Init(acn, sk, &sid) - - // Goroutine Management - sg := new(sync.WaitGroup) - sg.Add(1) - go func() { - service.Listen(NewTokenBoardServer(&tokenService, serverAuditableStore)) - sg.Done() - }() - - time.Sleep(time.Second * 30) - id, sk := primitives.InitializeEphemeralIdentity() - var client tapir.Service - client = new(tor.BaseOnionService) - client.Init(acn, sk, &id) - client.Connect(sid.Hostname(), NewTokenBoardClient(clientAuditableStore, Handler{Store: clientAuditableStore}, &FreePaymentHandler{TokenService: &tokenService})) - client.WaitForCapabilityOrClose(sid.Hostname(), AuthCapability) - conn, _ := client.GetConnection(sid.Hostname()) - tba, _ := conn.App().(*TokenBoardApp) - tba.PurchaseTokens(10) - tba.Post([]byte("HELLO 1")) - tba.Post([]byte("HELLO 2")) - tba.Post([]byte("HELLO 3")) - tba.Post([]byte("HELLO 4")) - tba.Post([]byte("HELLO 5")) - - tba.Replay() - tba.Post([]byte("HELLO 6")) - tba.Post([]byte("HELLO 7")) - tba.Post([]byte("HELLO 8")) - tba.Post([]byte("HELLO 9")) - tba.Post([]byte("HELLO 10")) - - tba.Replay() - - if tba.Post([]byte("HELLO 11")) { - t.Errorf("Post should have failed.") - } - - time.Sleep(time.Second * 60) - -} diff --git a/applications/transcript_app.go b/applications/transcript_app.go index 1b6c8ad..151df5d 100644 --- a/applications/transcript_app.go +++ b/applications/transcript_app.go @@ -5,7 +5,7 @@ import ( "cwtch.im/tapir/primitives/core" ) -// TranscriptApp defines a Tapir Meta=App which provides a global cryptographic transcript +// TranscriptApp defines a Tapir Meta-App which provides a global cryptographic transcript type TranscriptApp struct { transcript *core.Transcript } @@ -25,3 +25,8 @@ func (ta *TranscriptApp) Init(connection tapir.Connection) { func (ta *TranscriptApp) Transcript() *core.Transcript { return ta.transcript } + +// PropagateTranscript overrides the default transcript and propagates a transcript from a previous session +func (ta *TranscriptApp) PropagateTranscript(transcript *core.Transcript) { + ta.transcript = transcript +} diff --git a/go.mod b/go.mod index 2d5df8d..ecb300a 100644 --- a/go.mod +++ b/go.mod @@ -2,5 +2,11 @@ module cwtch.im/tapir require ( git.openprivacy.ca/openprivacy/libricochet-go v1.0.4 + github.com/gtank/merlin v0.1.1-0.20191105220539-8318aed1a79f + github.com/gtank/ristretto255 v0.1.1-0.20191011164322-af147e8e15b6 + go.etcd.io/bbolt v1.3.3 golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f + golang.org/x/sync v0.0.0-20190423024810-112230192c58 // indirect ) + +go 1.13 diff --git a/go.sum b/go.sum index 4973118..d518b0a 100644 --- a/go.sum +++ b/go.sum @@ -8,11 +8,21 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/gtank/merlin v0.1.1-0.20191105220539-8318aed1a79f h1:8N8XWLZelZNibkhM1FuF+3Ad3YIbgirjdMiVA0eUkaM= +github.com/gtank/merlin v0.1.1-0.20191105220539-8318aed1a79f/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s= +github.com/gtank/ristretto255 v0.1.0 h1:WQKpyRsq8Yt7dm0oq6Gj18BGku/Zbj/TOIolBYfmbiI= +github.com/gtank/ristretto255 v0.1.0/go.mod h1:Ph5OpO6c7xKUGROZfWVLiJf9icMDwUeIvY4OmlYW69o= +github.com/gtank/ristretto255 v0.1.1-0.20191011164322-af147e8e15b6 h1:fYrfnLiiWLCPvmmKbH8AlOwZAtrV0QDKox1HsAEjygY= +github.com/gtank/ristretto255 v0.1.1-0.20191011164322-af147e8e15b6/go.mod h1:Ph5OpO6c7xKUGROZfWVLiJf9icMDwUeIvY4OmlYW69o= +github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643 h1:hLDRPB66XQT/8+wG9WsDpiCvZf1yKO7sz7scAjSlBa0= +github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643/go.mod h1:43+3pMjjKimDBf5Kr4ZFNGbLql1zKkbImw+fZbw3geM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= golang.org/x/crypto v0.0.0-20190128193316-c7b33c32a30b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f h1:R423Cnkcp5JABoeemiGEPlt9tHXFfw5kvc0yqlxRPWo= @@ -20,8 +30,9 @@ golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8U golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/networks/tor/BaseOnionService.go b/networks/tor/BaseOnionService.go index 5d1a3ec..6a401cb 100644 --- a/networks/tor/BaseOnionService.go +++ b/networks/tor/BaseOnionService.go @@ -34,7 +34,7 @@ func (s *BaseOnionService) Init(acn connectivity.ACN, sk ed25519.PrivateKey, id // WaitForCapabilityOrClose blocks until the connection has the given capability or the underlying connection is closed // (through error or user action) -func (s *BaseOnionService) WaitForCapabilityOrClose(cid string, name string) (tapir.Connection, error) { +func (s *BaseOnionService) WaitForCapabilityOrClose(cid string, name tapir.Capability) (tapir.Connection, error) { conn, err := s.GetConnection(cid) if err == nil { for { diff --git a/persistence/bolt_persistence.go b/persistence/bolt_persistence.go new file mode 100644 index 0000000..5cc382f --- /dev/null +++ b/persistence/bolt_persistence.go @@ -0,0 +1,76 @@ +package persistence + +import ( + "encoding/json" + "git.openprivacy.ca/openprivacy/libricochet-go/log" + bolt "go.etcd.io/bbolt" +) + +// BoltPersistence creates a persistence services backed by an on-disk bolt database +type BoltPersistence struct { + db *bolt.DB +} + +// Open opens a database +func (bp *BoltPersistence) Open(handle string) error { + db, err := bolt.Open(handle, 0600, nil) + bp.db = db + log.Debugf("Loaded the Database") + return err +} + +// Setup initializes the given buckets if they do not exist in the database +func (bp *BoltPersistence) Setup(buckets []string) error { + return bp.db.Update(func(tx *bolt.Tx) error { + for _, bucket := range buckets { + tx.CreateBucketIfNotExists([]byte(bucket)) + } + return nil + }) +} + +// Close closes the databases +func (bp *BoltPersistence) Close() { + bp.db.Close() +} + +// Persist stores a record in the database +func (bp *BoltPersistence) Persist(bucket string, name string, value interface{}) error { + valueBytes, _ := json.Marshal(value) + return bp.db.Update(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte(bucket)) + b.Put([]byte(name), valueBytes) + return nil + }) +} + +// Check returns true if the record exists in the given bucket. +func (bp *BoltPersistence) Check(bucket string, name string) (bool, error) { + log.Debugf("Checking database: %v %v", bucket, name) + var val []byte + err := bp.db.View(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte(bucket)) + val = b.Get([]byte(name)) + return nil + }) + if err != nil { + return false, err + } else if val != nil { + return true, nil + } + return false, nil +} + +// Load reads a value from a given bucket. +func (bp *BoltPersistence) Load(bucket string, name string, value interface{}) error { + var val []byte + err := bp.db.View(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte(bucket)) + val = b.Get([]byte(name)) + return nil + }) + if err != nil { + return err + } + return json.Unmarshal(val, &value) +} diff --git a/persistence/bolt_persistence_test.go b/persistence/bolt_persistence_test.go new file mode 100644 index 0000000..b757474 --- /dev/null +++ b/persistence/bolt_persistence_test.go @@ -0,0 +1,23 @@ +package persistence + +import ( + "testing" +) + +func TestBoltPersistence_Open(t *testing.T) { + var db Service + db = new(BoltPersistence) + db.Open("test.dbgi") + db.Setup([]string{"tokens"}) + db.Persist("tokens", "random_value", true) + + var exists bool + db.Load("tokens", "random_value", &exists) + + if exists { + t.Logf("Successfully stored: %v", exists) + } else { + t.Fatalf("Failure to store record in DB!") + } + db.Close() +} diff --git a/persistence/persistence.go b/persistence/persistence.go new file mode 100644 index 0000000..0dee4b0 --- /dev/null +++ b/persistence/persistence.go @@ -0,0 +1,11 @@ +package persistence + +// Service provides a consistent interface for interacting with on-disk, in-memory or server-backed storage +type Service interface { + Open(handle string) error + Setup(buckets []string) error + Persist(bucket string, name string, value interface{}) error + Check(bucket string, name string) (bool, error) + Load(bucket string, name string, value interface{}) error + Close() +} diff --git a/primitives/auditablestore.go b/primitives/auditable/auditablestore.go similarity index 51% rename from primitives/auditablestore.go rename to primitives/auditable/auditablestore.go index bd33593..ca0b52d 100644 --- a/primitives/auditablestore.go +++ b/primitives/auditable/auditablestore.go @@ -1,64 +1,113 @@ -package primitives +package auditable import ( + "cwtch.im/tapir/persistence" + "cwtch.im/tapir/primitives" "cwtch.im/tapir/primitives/core" "encoding/base64" "errors" + "git.openprivacy.ca/openprivacy/libricochet-go/log" "golang.org/x/crypto/ed25519" "sync" ) // SignedProof encapsulates a signed proof -type SignedProof struct { - Commit []byte - Proof []byte -} +type SignedProof []byte // Message encapsulates a message for more readable code. type Message []byte // State defines an array of messages. type State struct { - Messages []Message + SignedProof SignedProof + Messages []Message } -// AuditableStore defines a cryptographically secure & auditable transcript of messages sent from multiple +// +const ( + auditableDataStoreProtocol = "auditable-data-store" + newMessage = "new-message" + commit = "commit" + collapse = "collapse" +) + +// Store defines a cryptographically secure & auditable transcript of messages sent from multiple // unrelated clients to a server. -type AuditableStore struct { +type Store struct { state State - identity Identity + identity primitives.Identity transcript *core.Transcript LatestCommit []byte commits map[string]int mutex sync.Mutex + db persistence.Service } // Init initializes an auditable store -func (as *AuditableStore) Init(identity Identity) { +func (as *Store) Init(identity primitives.Identity) { as.identity = identity - as.transcript = core.NewTranscript("auditable-data-store") + as.transcript = core.NewTranscript(auditableDataStoreProtocol) as.commits = make(map[string]int) } +const messageBucket = "auditable-messages" + +// LoadFromStorage initializes an auditable store from a DB +func (as *Store) LoadFromStorage(db persistence.Service) { + db.Setup([]string{messageBucket}) + var messages []Message + db.Load(messageBucket, "messages", &messages) + log.Debugf("Loaded from Database: %v", len(messages)) + for _, message := range messages { + as.add(message) + } + log.Debugf("Loaded %v Messages from the Database", len(messages)) + as.db = db +} + // Add adds a message to the auditable store -func (as *AuditableStore) Add(message Message) SignedProof { +func (as *Store) Add(message Message) SignedProof { + sp := as.add(message) + if as.db != nil { + as.db.Persist(messageBucket, "messages", as.state.Messages) + } + return sp +} + +// Add adds a message to the auditable store +func (as *Store) add(message Message) SignedProof { as.mutex.Lock() defer as.mutex.Unlock() + as.transcript.AddToTranscript(newMessage, message) + as.LatestCommit = as.transcript.CommitToTranscript(commit) + as.state.Messages = append(as.state.Messages, message) - as.transcript.AddToTranscript("new-message", message) - as.LatestCommit = as.identity.Sign(as.transcript.CommitToTranscript("commit")) - return SignedProof{as.LatestCommit, as.identity.Sign(as.LatestCommit)} + as.state.SignedProof = as.identity.Sign(as.LatestCommit) + + as.commits[base64.StdEncoding.EncodeToString(as.LatestCommit)] = len(as.state.Messages) - 1 + return as.state.SignedProof } // GetState returns the current auditable state -func (as *AuditableStore) GetState() (State, SignedProof) { +func (as *Store) GetState() State { as.mutex.Lock() defer as.mutex.Unlock() - return as.state, SignedProof{as.LatestCommit, as.identity.Sign(as.LatestCommit)} + return as.state +} + +// GetStateAfter returns the current auditable state after a given commitment +func (as *Store) GetStateAfter(commitment []byte) State { + if commitment == nil { + return as.GetState() + } + var state State + state.Messages = as.GetMessagesAfter(commitment) + state.SignedProof = as.identity.Sign(as.LatestCommit) + return state } // GetMessagesAfter provides access to messages after the given commit. -func (as *AuditableStore) GetMessagesAfter(latestCommit []byte) []Message { +func (as *Store) GetMessagesAfter(latestCommit []byte) []Message { as.mutex.Lock() defer as.mutex.Unlock() index, ok := as.commits[base64.StdEncoding.EncodeToString(latestCommit)] @@ -70,27 +119,33 @@ func (as *AuditableStore) GetMessagesAfter(latestCommit []byte) []Message { return as.state.Messages[index+1:] } -// MergeState merges a given state onto our state, first verifying that the two transcripts align -func (as *AuditableStore) MergeState(state State, signedStateProof SignedProof) error { +// AppendState merges a given state onto our state, first verifying that the two transcripts align +func (as *Store) AppendState(state State) error { next := len(as.state.Messages) - for i, m := range state.Messages[next:] { + for i, m := range state.Messages { as.state.Messages = append(as.state.Messages, m) // We reconstruct the transcript - as.transcript.AddToTranscript("new-message", m) - as.LatestCommit = as.identity.Sign(as.transcript.CommitToTranscript("commit")) + as.transcript.AddToTranscript(newMessage, m) + as.LatestCommit = as.transcript.CommitToTranscript(commit) + log.Debugf("Adding message %d commit: %x", next+i, as.LatestCommit) as.commits[base64.StdEncoding.EncodeToString(as.LatestCommit)] = next + i } // verify that our state matches the servers signed state // this is *not* a security check, as a rogue server can simply sign any state // however committing to a state allows us to build fraud proofs for malicious servers later on. - if ed25519.Verify(as.identity.PublicKey(), as.LatestCommit, signedStateProof.Proof) == false { + if ed25519.Verify(as.identity.PublicKey(), as.LatestCommit, state.SignedProof) == false { return errors.New("state is not consistent, the server is malicious") } return nil } +// MergeState merges a given state onto our state, first verifying that the two transcripts align +func (as *Store) MergeState(state State) error { + return as.AppendState(State{Messages: state.Messages[len(as.state.Messages):], SignedProof: state.SignedProof}) +} + // VerifyFraudProof - the main idea behind this is as follows: // // Every update requires the server to sign, and thus commit to, a transcript @@ -101,16 +156,16 @@ func (as *AuditableStore) MergeState(state State, signedStateProof SignedProof) // If, after syncing, the FraudProof still validates, then the server must be malicious. // the information revealed by publicizing a fraud proof is minimal it only reveals the inconsistent transcript commit // and not the cause (which could be reordered messages, dropped messages, additional messages or any combination) -func (as *AuditableStore) VerifyFraudProof(signedFraudProof SignedProof, key ed25519.PublicKey) (bool, error) { +func (as *Store) VerifyFraudProof(fraudCommit []byte, signedFraudProof SignedProof, key ed25519.PublicKey) (bool, error) { - if ed25519.Verify(key, signedFraudProof.Commit, signedFraudProof.Proof) == false { + if ed25519.Verify(key, fraudCommit, signedFraudProof) == false { // This could happen due to misuse of this function (trying to verify a proof with the wrong public key) // This could happen if the server lies to us and submits a fake state proof, however we cannot use this to // prove that the server is acting maliciously return false, errors.New("signed proof has not been signed by the given public key") } - _, exists := as.commits[base64.StdEncoding.EncodeToString(signedFraudProof.Commit)] + _, exists := as.commits[base64.StdEncoding.EncodeToString(fraudCommit)] if !exists { // We have a message signed by the server which verifies that a message was inserted into the state at a given index // However this directly contradicts our version of the state. @@ -121,3 +176,10 @@ func (as *AuditableStore) VerifyFraudProof(signedFraudProof SignedProof, key ed2 return false, nil } + +// Collapse constructs a verifiable proof stating that the server has collapsed the previous history into the current +// root = H(onion) +// L = H(Sign(LatestCommit)) +func (as *Store) Collapse() { + as.LatestCommit = as.identity.Sign(as.transcript.CommitToTranscript(collapse)) +} diff --git a/primitives/auditable/auditablestore_test.go b/primitives/auditable/auditablestore_test.go new file mode 100644 index 0000000..f316fb4 --- /dev/null +++ b/primitives/auditable/auditablestore_test.go @@ -0,0 +1,70 @@ +package auditable + +import ( + "cwtch.im/tapir/persistence" + "cwtch.im/tapir/primitives" + "fmt" + "git.openprivacy.ca/openprivacy/libricochet-go/log" + "os" + "testing" +) + +func BenchmarkAuditableStore(b *testing.B) { + log.SetLevel(log.LevelDebug) + os.Remove("benchmark-auditablestore.db") + + as := new(Store) + serverID, _ := primitives.InitializeEphemeralIdentity() + as.Init(serverID) + + db := new(persistence.BoltPersistence) + db.Open("benchmark-auditablestore.db") + + as.LoadFromStorage(db) + + for i := 0; i < b.N; i++ { + data := fmt.Sprintf("Message %v", i) + as.Add(Message(data)) + } + db.Close() + db.Open("benchmark-auditablestore.db") + vs := new(Store) + vs.Init(serverID) + vs.LoadFromStorage(db) + db.Close() + os.Remove("benchmark-auditablestore.db") +} + +func TestAuditableStore(t *testing.T) { + as := new(Store) + vs := new(Store) + + serverID, _ := primitives.InitializeEphemeralIdentity() + as.Init(serverID) + vs.Init(serverID) // This doesn't do anything + + as.Add([]byte("Hello World")) + state := as.GetState() + + if vs.MergeState(state) != nil { + t.Fatalf("Fraud Proof Failed on Honest Proof") + } + + fraudProof := as.Add([]byte("Hello World 2")) + + // If you comment these out it simulates a lying server. + state = as.GetState() + if vs.MergeState(state) != nil { + t.Fatalf("Fraud Proof Failed on Honest Proof") + } + + fraud, err := vs.VerifyFraudProof(as.LatestCommit, fraudProof, serverID.PublicKey()) + + if err != nil { + t.Fatalf("Error validated fraud proof: %v", err) + } + + if fraud { + t.Fatalf("Technically a fraud, but the client hasn't updated yet") + } +} diff --git a/primitives/auditablestore_test.go b/primitives/auditablestore_test.go deleted file mode 100644 index 2ffe990..0000000 --- a/primitives/auditablestore_test.go +++ /dev/null @@ -1,39 +0,0 @@ -package primitives - -import ( - "testing" -) - -func TestAuditableStore(t *testing.T) { - as := new(AuditableStore) - vs := new(AuditableStore) - - serverID, _ := InitializeEphemeralIdentity() - as.Init(serverID) - vs.Init(serverID) // This doesn't do anything - - as.Add([]byte("Hello World")) - state, proof := as.GetState() - - if vs.MergeState(state, proof) != nil { - t.Fatalf("Fraud Proof Failed on Honest Proof") - } - - fraudProof := as.Add([]byte("Hello World 2")) - - // If you comment these out it simulates a lying server. - state, proof = as.GetState() - if vs.MergeState(state, proof) != nil { - t.Fatalf("Fraud Proof Failed on Honest Proof") - } - - fraud, err := vs.VerifyFraudProof(fraudProof, serverID.PublicKey()) - - if err != nil { - t.Fatalf("Error validated fraud proof: %v", err) - } - - if fraud { - t.Fatalf("Technically a fraud, but the client hasn't updated yet") - } -} diff --git a/primitives/core/operations.go b/primitives/core/operations.go new file mode 100644 index 0000000..61c3bdd --- /dev/null +++ b/primitives/core/operations.go @@ -0,0 +1,186 @@ +package core + +import ( + "fmt" + ristretto "github.com/gtank/ristretto255" +) + +// ScalarVector explicit type checking +type ScalarVector []*ristretto.Scalar + +// PointVector explicit type checking +type PointVector []*ristretto.Element + +// GeneratorVector explicit type checking +type GeneratorVector []*ristretto.Element + +// CopyVector safely copies a vector +func CopyVector(G GeneratorVector) GeneratorVector { + H := make(GeneratorVector, len(G)) + for i, g := range G { + H[i] = new(ristretto.Element).Add(new(ristretto.Element).Zero(), g) + } + return H +} + +// InnerProduct takes the inner product of a and b i.e. +func InnerProduct(a, b ScalarVector) *ristretto.Scalar { + if len(a) != len(b) { + panic(fmt.Sprintf("len(a) = %v ; len(b) = %v;", len(a), len(b))) + } + + result := new(ristretto.Scalar).Zero() + for i, ai := range a { + result.Add(result, new(ristretto.Scalar).Multiply(ai, b[i])) + } + return result +} + +// MultiExp takes in a vector of scalars = {a,b,c...} and a vector of generator = {A,B,C...} and outputs +// {aA,bB,cC} +func MultiExp(a ScalarVector, G GeneratorVector) *ristretto.Element { + if len(a) > len(G) { + panic(fmt.Sprintf("len(a) = %v ; len(b) = %v;", len(a), len(G))) + } + result := new(ristretto.Element).Zero() + for i, ai := range a { + aG := new(ristretto.Element).ScalarMult(ai, G[i]) + result = new(ristretto.Element).Add(result, aG) + } + return result +} + +// SafeAppend is defined for a vector of Scalars +func (a ScalarVector) SafeAppend(b *ristretto.Scalar) ScalarVector { + list := make(ScalarVector, len(a)+1) + for i := 0; i < len(a); i++ { + list[i] = a[i] + } + list[len(a)] = b + return list +} + +// Join is defined for a vector of Scalars +func (a ScalarVector) Join(b ScalarVector) ScalarVector { + list := make(ScalarVector, len(a)+len(b)) + for i := 0; i < len(a); i++ { + list[i] = a[i] + } + for i := len(a); i < len(a)+len(b); i++ { + list[i] = b[i-len(a)] + } + return list +} + +// SafeAppend as defined for a vector of Generators +func (a GeneratorVector) SafeAppend(b *ristretto.Element) GeneratorVector { + list := make(GeneratorVector, len(a)+1) + for i := 0; i < len(a); i++ { + list[i] = a[i] + } + list[len(a)] = b + return list +} + +// Join as defined for a vector of Generators +func (a GeneratorVector) Join(b GeneratorVector) GeneratorVector { + list := make(GeneratorVector, len(a)+len(b)) + for i := 0; i < len(a); i++ { + list[i] = a[i] + } + for i := len(a); i < len(a)+len(b); i++ { + list[i] = b[i-len(a)] + } + return list +} + +// VectorAddScalar takes in a vector v = {a,b,c..} and a scalar s and outputs {a+s,b+s,c+s....} +func VectorAddScalar(vector ScalarVector, scalar *ristretto.Scalar) ScalarVector { + result := make(ScalarVector, len(vector)) + for i := range vector { + result[i] = new(ristretto.Scalar) + result[i].Add(vector[i], scalar) + } + return result +} + +// VectorNegate takes in a vector v = {a,b,c..} and a scalar s and outputs {-a,-b,-c} +func VectorNegate(vector ScalarVector) ScalarVector { + result := make(ScalarVector, len(vector)) + for i := range vector { + result[i] = new(ristretto.Scalar).Negate(vector[i]) + } + return result +} + +// VectorMulScalar takes in a vector v = {a,b,c..} and a scalar s and outputs {as,bs,cs....} +func VectorMulScalar(vector ScalarVector, scalar *ristretto.Scalar) ScalarVector { + result := make(ScalarVector, len(vector)) + for i := range vector { + result[i] = new(ristretto.Scalar) + result[i].Multiply(vector[i], scalar) + } + return result +} + +// EntrywiseSum takes the entry wise sum of two vectors +func EntrywiseSum(vector ScalarVector, vector2 ScalarVector) ScalarVector { + result := make(ScalarVector, len(vector)) + for i, v := range vector { + result[i] = new(ristretto.Scalar) + result[i].Add(v, vector2[i]) + } + return result +} + +// EntrywiseSub takes the entry wise subtraction of two vectors +func EntrywiseSub(vector ScalarVector, vector2 ScalarVector) ScalarVector { + result := make(ScalarVector, len(vector)) + for i, v := range vector { + result[i] = new(ristretto.Scalar) + result[i].Subtract(v, vector2[i]) + } + return result +} + +// EntryWiseProduct takes the entry wise product of two vectors +func EntryWiseProduct(vector ScalarVector, vector2 ScalarVector) ScalarVector { + result := make(ScalarVector, len(vector)) + for i, v := range vector { + result[i] = new(ristretto.Scalar) + result[i].Multiply(v, vector2[i]) + } + return result +} + +// One returns a ristretto scalar == 1 +func One() *ristretto.Scalar { + one := new(ristretto.Scalar) + one.Decode([]byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}) + return one +} + +// IdentityVector is a convenience function to generate a vector v = {1,1,1...1} +func IdentityVector(n int) ScalarVector { + result := make(ScalarVector, n) + one := new(ristretto.Scalar) + one.Decode([]byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}) + for i := 0; i < n; i++ { + result[i] = one + } + return result +} + +// PowerVector creates a vector v = {1,x,x^2,x^3..x^n} +func PowerVector(x *ristretto.Scalar, n int) ScalarVector { + result := make(ScalarVector, n) + one := new(ristretto.Scalar) + one.Decode([]byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}) + result[0] = one + result[1] = x + for i := 2; i < n; i++ { + result[i] = new(ristretto.Scalar) + result[i].Multiply(result[i-1], x) + } + return result +} diff --git a/primitives/core/transcript.go b/primitives/core/transcript.go index f2d6450..b05c59e 100644 --- a/primitives/core/transcript.go +++ b/primitives/core/transcript.go @@ -2,32 +2,28 @@ package core import ( "fmt" - "github.com/bwesterb/go-ristretto" + "github.com/gtank/merlin" + ristretto "github.com/gtank/ristretto255" "golang.org/x/crypto/sha3" - "hash" "io" ) -// Transcript implements a transcript of a public coin argument. +// Transcript provides a consistent transcript primitive for our protocols // // We have the following goals: -// - Provide a consisted transcript API for our zero knowledge protocols // - Allow sequential proofs over a common transcript (ensuring a single proof cannot be extracted standalone) -// - produce an auditable human-readable transcript. +// - be able to produce a human-readable transcript for auditing. // // The design of this API was inspired by Merlin: https://docs.rs/crate/merlin/ -// -// At some point we might want to extend this to be compatible with Merlin transcripts, built on STROBE type Transcript struct { - hash hash.Hash - transcript string + merlinTranscript *merlin.Transcript + transcript string } // NewTranscript creates a new Transcript with the given Label, the label should be unique to the application func NewTranscript(label string) *Transcript { transcript := new(Transcript) - transcript.hash = sha3.New256() - transcript.AddToTranscript("protocol", []byte(label)) + transcript.merlinTranscript = merlin.NewTranscript(label) return transcript } @@ -36,7 +32,13 @@ func NewTranscript(label string) *Transcript { func (t *Transcript) AddToTranscript(label string, b []byte) { op := fmt.Sprintf("%s (%d) %x;", label, len(b), b) t.transcript = fmt.Sprintf("%v\n%v", t.transcript, op) - t.hash.Write([]byte(op)) + t.merlinTranscript.AppendMessage([]byte(label), b) +} + +// AddElementToTranscript appends a value to the transcript with the given label +// This binds the given data to the label. +func (t *Transcript) AddElementToTranscript(label string, element *ristretto.Element) { + t.AddToTranscript(label, element.Encode([]byte{})) } // OutputTranscriptToAudit outputs a human-readable copy of the transcript so far. @@ -44,11 +46,18 @@ func (t Transcript) OutputTranscriptToAudit() string { return t.transcript } +// NewProtocol provides explicit protocol separation in a transcript (more readable audit scripts and even more explicit +// binding of committed values to a given context) +func (t *Transcript) NewProtocol(label string) { + op := fmt.Sprintf("---- new-protcol: %s ----", label) + t.transcript = fmt.Sprintf("%v\n%v", t.transcript, op) + t.merlinTranscript.AppendMessage([]byte("protocol"), []byte(label)) +} + // CommitToTranscript generates a challenge based on the current transcript, it also commits the challenge to the transcript. func (t *Transcript) CommitToTranscript(label string) []byte { - t.AddToTranscript("commit", []byte(label)) - b := t.hash.Sum([]byte{}) - t.AddToTranscript(label, b) + b := t.merlinTranscript.ExtractBytes([]byte(label), 64) + t.transcript = fmt.Sprintf("%v\nextract %v: %v", t.transcript, label, b) return b } @@ -59,25 +68,39 @@ type PRNG struct { // Next returns the next "random" scalar from the PRNG func (prng *PRNG) Next() *ristretto.Scalar { - buf := [32]byte{} + buf := [64]byte{} io.ReadFull(prng.prng, buf[:]) - return new(ristretto.Scalar).SetBytes(&buf) + next := new(ristretto.Scalar) + next.FromUniformBytes(buf[:]) + return next } // CommitToPRNG commits the label to the transcript and derives a PRNG from the transcript. func (t *Transcript) CommitToPRNG(label string) PRNG { - t.AddToTranscript("commit-prng", []byte(label)) - b := t.hash.Sum([]byte{}) - t.AddToTranscript(label, b) + b := t.merlinTranscript.ExtractBytes([]byte(label), 64) prng := sha3.NewShake256() prng.Write(b) return PRNG{prng: prng} } +// CommitToGenerator derives a verifiably random generator from the transcript +func (t *Transcript) CommitToGenerator(label string) *ristretto.Element { + c := t.CommitToTranscript(label) + return new(ristretto.Element).FromUniformBytes(c) +} + +// CommitToGenerators derives a set of verifiably random generators from the transcript +func (t *Transcript) CommitToGenerators(label string, n int) (generators []*ristretto.Element) { + for i := 0; i < n; i++ { + generators = append(generators, t.CommitToGenerator(fmt.Sprintf("%v-%d", label, i))) + } + return generators +} + // CommitToTranscriptScalar is a convenience method for CommitToTranscript which returns a ristretto Scalar func (t *Transcript) CommitToTranscriptScalar(label string) *ristretto.Scalar { c := t.CommitToTranscript(label) - cs := [32]byte{} - copy(cs[:], c[:]) - return new(ristretto.Scalar).SetBytes(&cs) + s := new(ristretto.Scalar) + s.FromUniformBytes(c[:]) + return s } diff --git a/primitives/dlog.go b/primitives/dlog.go deleted file mode 100644 index f89b7c0..0000000 --- a/primitives/dlog.go +++ /dev/null @@ -1,71 +0,0 @@ -package primitives - -import ( - "cwtch.im/tapir/primitives/core" - "github.com/bwesterb/go-ristretto" -) - -// DLProof Encapsulates a Discrete Log / Schnorr Proof -// Note that these parameters are read-only. -type DLProof struct { - V, A ristretto.Point - R ristretto.Scalar -} - -// DiscreteLogProof - Proof of Knowledge of Exponent -// Given V = xG -// Peggy: z := choose randomly from Zq -// A := zG -// c := H(transcript(G,V,A)) mod q -// r := (z + cx) mod q -// -// Sends A,r,V to Vicky -func DiscreteLogProof(x ristretto.Scalar, v ristretto.Point, transcript *core.Transcript) (proof DLProof) { - - transcript.AddToTranscript("G", new(ristretto.Point).SetBase().Bytes()) - - // We bind the proof to our public V - proof.V = v - transcript.AddToTranscript("V", proof.V.Bytes()) - - // Generate a random z - // A := zG - z := new(ristretto.Scalar).Rand() - proof.A = *new(ristretto.Point).ScalarMultBase(z) - transcript.AddToTranscript("A", proof.A.Bytes()) - - // Derive Challenge - c := transcript.CommitToTranscriptScalar("c") - - // r := (z + cx) mod p - cx := new(ristretto.Scalar).Mul(c, &x) - proof.R = *new(ristretto.Scalar).Add(z, cx) - - return -} - -// VerifyDiscreteLogProof validates a given Schnorr Proof -// Vicky gets A,r,V from Peggy -// Vicky computes c := H(transcript(G,V,A)) mod q -// Vicky checks rG := A + cV -// rG ?= zG + cV -// (z+cx)G ?= zG + cV -// ?= zG + cxG -// Thus demonstrating that Peggy knows the discrete log to V -func VerifyDiscreteLogProof(proof DLProof, transcript *core.Transcript) bool { - - transcript.AddToTranscript("G", new(ristretto.Point).SetBase().Bytes()) - transcript.AddToTranscript("V", proof.V.Bytes()) - transcript.AddToTranscript("A", proof.A.Bytes()) - c := transcript.CommitToTranscriptScalar("c") - - // Compute left hand side - lhs := new(ristretto.Point).ScalarMultBase(&proof.R) - - // Compute right hand side - cV := new(ristretto.Point).ScalarMult(&proof.V, c) - rhs := new(ristretto.Point).Add(&proof.A, cV) - - // Result of verification: lhs ?= rhs - return lhs.Equals(rhs) -} diff --git a/primitives/privacypass/common.go b/primitives/privacypass/common.go new file mode 100644 index 0000000..3f6c91e --- /dev/null +++ b/primitives/privacypass/common.go @@ -0,0 +1,17 @@ +package privacypass + +// Transcript Constants +const ( + BatchProofProtocol = "privacy-pass-batch-proof" + BatchProofX = "X-batch" + BatchProofY = "Y-batch" + BatchProofPVector = "P-vector" + BatchProofQVector = "Q-vector" + + DLEQX = "X" + DLEQY = "Y" + DLEQP = "P" + DLEQQ = "Q" + DLEQA = "A" + DLEQB = "B" +) diff --git a/primitives/privacypass/dlogeq.go b/primitives/privacypass/dlogeq.go index 89caa1e..e4108ab 100644 --- a/primitives/privacypass/dlogeq.go +++ b/primitives/privacypass/dlogeq.go @@ -1,12 +1,13 @@ package privacypass import ( + "crypto/rand" "cwtch.im/tapir/primitives/core" - "github.com/bwesterb/go-ristretto" + ristretto "github.com/gtank/ristretto255" ) // DLEQProof encapsulates a Chaum-Pedersen DLEQ Proof -// David Chaum and Torben P. Pedersen. Wallet databaseswith observers. In Ernest F. Brickell, editor,CRYPTO’92,volume 740 ofLNCS, pages 89–105. Springer, Heidelberg,August 1993 +//gut In Ernest F. Brickell, editor,CRYPTO’92,volume 740 ofLNCS, pages 89–105. Springer, Heidelberg,August 1993 type DLEQProof struct { C *ristretto.Scalar S *ristretto.Scalar @@ -21,20 +22,23 @@ type DLEQProof struct { // s := (t + ck) mod q // // Sends c,s to Vicky -func DiscreteLogEquivalenceProof(k *ristretto.Scalar, X *ristretto.Point, Y *ristretto.Point, P *ristretto.Point, Q *ristretto.Point, transcript *core.Transcript) DLEQProof { - t := new(ristretto.Scalar).Rand() - A := new(ristretto.Point).ScalarMult(X, t) - B := new(ristretto.Point).ScalarMult(P, t) +func DiscreteLogEquivalenceProof(k *ristretto.Scalar, X *ristretto.Element, Y *ristretto.Element, P *ristretto.Element, Q *ristretto.Element, transcript *core.Transcript) DLEQProof { + private := make([]byte, 64) + rand.Read(private) + t := new(ristretto.Scalar) + t.FromUniformBytes(private) + A := new(ristretto.Element).ScalarMult(t, X) + B := new(ristretto.Element).ScalarMult(t, P) - transcript.AddToTranscript("X", X.Bytes()) - transcript.AddToTranscript("Y", Y.Bytes()) - transcript.AddToTranscript("P", P.Bytes()) - transcript.AddToTranscript("Q", Q.Bytes()) - transcript.AddToTranscript("A", A.Bytes()) - transcript.AddToTranscript("B", B.Bytes()) + transcript.AddToTranscript(DLEQX, X.Encode(nil)) + transcript.AddToTranscript(DLEQY, Y.Encode(nil)) + transcript.AddToTranscript(DLEQP, P.Encode(nil)) + transcript.AddToTranscript(DLEQQ, Q.Encode(nil)) + transcript.AddToTranscript(DLEQA, A.Encode(nil)) + transcript.AddToTranscript(DLEQB, B.Encode(nil)) c := transcript.CommitToTranscriptScalar("c") - s := new(ristretto.Scalar).Sub(t, new(ristretto.Scalar).Mul(c, k)) + s := new(ristretto.Scalar).Subtract(t, new(ristretto.Scalar).Multiply(c, k)) return DLEQProof{c, s} } @@ -48,22 +52,22 @@ func DiscreteLogEquivalenceProof(k *ristretto.Scalar, X *ristretto.Point, Y *ris // B' = P'+Q' == sP + cQ ?= sP + ckP == (s+ck)P == tP == B // c' := H(transcript(X,Y,P,Q,A',B')) // Tests c ?= c -func VerifyDiscreteLogEquivalenceProof(dleq DLEQProof, X *ristretto.Point, Y *ristretto.Point, P *ristretto.Point, Q *ristretto.Point, transcript *core.Transcript) bool { +func VerifyDiscreteLogEquivalenceProof(dleq DLEQProof, X *ristretto.Element, Y *ristretto.Element, P *ristretto.Element, Q *ristretto.Element, transcript *core.Transcript) bool { - Xs := new(ristretto.Point).ScalarMult(X, dleq.S) - Yc := new(ristretto.Point).ScalarMult(Y, dleq.C) - Ps := new(ristretto.Point).ScalarMult(P, dleq.S) - Qc := new(ristretto.Point).ScalarMult(Q, dleq.C) + Xs := new(ristretto.Element).ScalarMult(dleq.S, X) + Yc := new(ristretto.Element).ScalarMult(dleq.C, Y) + Ps := new(ristretto.Element).ScalarMult(dleq.S, P) + Qc := new(ristretto.Element).ScalarMult(dleq.C, Q) - A := new(ristretto.Point).Add(Xs, Yc) - B := new(ristretto.Point).Add(Ps, Qc) + A := new(ristretto.Element).Add(Xs, Yc) + B := new(ristretto.Element).Add(Ps, Qc) - transcript.AddToTranscript("X", X.Bytes()) - transcript.AddToTranscript("Y", Y.Bytes()) - transcript.AddToTranscript("P", P.Bytes()) - transcript.AddToTranscript("Q", Q.Bytes()) - transcript.AddToTranscript("A", A.Bytes()) - transcript.AddToTranscript("B", B.Bytes()) + transcript.AddToTranscript(DLEQX, X.Encode(nil)) + transcript.AddToTranscript(DLEQY, Y.Encode(nil)) + transcript.AddToTranscript(DLEQP, P.Encode(nil)) + transcript.AddToTranscript(DLEQQ, Q.Encode(nil)) + transcript.AddToTranscript(DLEQA, A.Encode(nil)) + transcript.AddToTranscript(DLEQB, B.Encode(nil)) - return transcript.CommitToTranscriptScalar("c").Equals(dleq.C) + return transcript.CommitToTranscriptScalar("c").Equal(dleq.C) == 1 } diff --git a/primitives/privacypass/token.go b/primitives/privacypass/token.go index c9d6bd6..4dc8829 100644 --- a/primitives/privacypass/token.go +++ b/primitives/privacypass/token.go @@ -6,7 +6,8 @@ import ( "cwtch.im/tapir/primitives/core" "fmt" "git.openprivacy.ca/openprivacy/libricochet-go/log" - "github.com/bwesterb/go-ristretto" + ristretto "github.com/gtank/ristretto255" + "golang.org/x/crypto/sha3" ) @@ -15,17 +16,17 @@ import ( type Token struct { t []byte r *ristretto.Scalar - W *ristretto.Point + W *ristretto.Element } // BlindedToken encapsulates a Blinded Token type BlindedToken struct { - P *ristretto.Point + P *ristretto.Element } // SignedToken encapsulates a Signed (Blinded) Token type SignedToken struct { - Q *ristretto.Point + Q *ristretto.Element } // SpentToken encapsulates the parameters needed to spend a Token @@ -36,7 +37,7 @@ type SpentToken struct { // TokenPaymentHandler defines an interface with external payment processors type TokenPaymentHandler interface { - MakePayment(int) + MakePayment() NextToken(data []byte) (SpentToken, error) } @@ -45,23 +46,26 @@ type TokenPaymentHandler interface { func (t *Token) GenBlindedToken() BlindedToken { t.t = make([]byte, 32) rand.Read(t.t) - t.r = new(ristretto.Scalar).Rand() + t.r = new(ristretto.Scalar) + b := make([]byte, 64) + rand.Read(b) + t.r.FromUniformBytes(b) - Ht := sha3.Sum256(t.t) + Ht := sha3.Sum512(t.t) log.Debugf("token: %x", Ht) - T := new(ristretto.Point).SetElligator(&Ht) - P := new(ristretto.Point).ScalarMult(T, t.r) + T := new(ristretto.Element).FromUniformBytes(Ht[:]) + P := new(ristretto.Element).ScalarMult(t.r, T) return BlindedToken{P} } // unblindSignedToken unblinds a token that has been signed by a server func (t *Token) unblindSignedToken(token SignedToken) { - t.W = new(ristretto.Point).ScalarMult(token.Q, new(ristretto.Scalar).Inverse(t.r)) + t.W = new(ristretto.Element).ScalarMult(new(ristretto.Scalar).Invert(t.r), token.Q) } // SpendToken binds the token with data and then redeems the token func (t *Token) SpendToken(data []byte) SpentToken { - key := sha3.Sum256(append(t.t, t.W.Bytes()...)) + key := sha3.Sum256(append(t.t, t.W.Encode(nil)...)) mac := hmac.New(sha3.New512, key[:]) return SpentToken{t.t, mac.Sum(data)} } @@ -76,27 +80,29 @@ func GenerateBlindedTokenBatch(num int) (tokens []*Token, blindedTokens []Blinde } // verifyBatchProof verifies a given batch proof (see also UnblindSignedTokenBatch) -func verifyBatchProof(dleq DLEQProof, Y *ristretto.Point, blindedTokens []BlindedToken, signedTokens []SignedToken, transcript *core.Transcript) bool { - transcript.AddToTranscript("X", new(ristretto.Point).SetBase().Bytes()) - transcript.AddToTranscript("Y", Y.Bytes()) - transcript.AddToTranscript("P[]", []byte(fmt.Sprintf("%v", blindedTokens))) - transcript.AddToTranscript("Q[]", []byte(fmt.Sprintf("%v", signedTokens))) +func verifyBatchProof(dleq DLEQProof, Y *ristretto.Element, blindedTokens []BlindedToken, signedTokens []SignedToken, transcript *core.Transcript) bool { + transcript.NewProtocol(BatchProofProtocol) + transcript.AddToTranscript(BatchProofX, new(ristretto.Element).Base().Encode(nil)) + transcript.AddToTranscript(BatchProofY, Y.Encode(nil)) + transcript.AddToTranscript(BatchProofPVector, []byte(fmt.Sprintf("%v", blindedTokens))) + transcript.AddToTranscript(BatchProofQVector, []byte(fmt.Sprintf("%v", signedTokens))) prng := transcript.CommitToPRNG("w") - M := new(ristretto.Point).SetZero() - Z := new(ristretto.Point).SetZero() + M := new(ristretto.Element).Zero() + Z := new(ristretto.Element).Zero() for i := range blindedTokens { c := prng.Next() - M = new(ristretto.Point).Add(new(ristretto.Point).ScalarMult(blindedTokens[i].P, c), M) - Z = new(ristretto.Point).Add(new(ristretto.Point).ScalarMult(signedTokens[i].Q, c), Z) + M = new(ristretto.Element).Add(new(ristretto.Element).ScalarMult(c, blindedTokens[i].P), M) + Z = new(ristretto.Element).Add(new(ristretto.Element).ScalarMult(c, signedTokens[i].Q), Z) } - return VerifyDiscreteLogEquivalenceProof(dleq, new(ristretto.Point).SetBase(), Y, M, Z, transcript) + return VerifyDiscreteLogEquivalenceProof(dleq, new(ristretto.Element).Base(), Y, M, Z, transcript) } // UnblindSignedTokenBatch taking in a set of tokens, their blinded & signed counterparts, a server public key (Y), a DLEQ proof and a transcript // verifies that the signing procedure has taken place correctly and unblinds the tokens. -func UnblindSignedTokenBatch(tokens []*Token, blindedTokens []BlindedToken, signedTokens []SignedToken, Y *ristretto.Point, proof DLEQProof, transcript *core.Transcript) bool { +func UnblindSignedTokenBatch(tokens []*Token, blindedTokens []BlindedToken, signedTokens []SignedToken, Y *ristretto.Element, proof DLEQProof, transcript *core.Transcript) bool { verified := verifyBatchProof(proof, Y, blindedTokens, signedTokens, transcript) if !verified { + log.Debugf(transcript.OutputTranscriptToAudit()) return false } for i, t := range tokens { diff --git a/primitives/privacypass/token_test.go b/primitives/privacypass/token_test.go index 31b69a1..9a23596 100644 --- a/primitives/privacypass/token_test.go +++ b/primitives/privacypass/token_test.go @@ -1,6 +1,7 @@ package privacypass import ( + "cwtch.im/tapir/persistence" "cwtch.im/tapir/primitives/core" "git.openprivacy.ca/openprivacy/libricochet-go/log" "testing" @@ -17,30 +18,32 @@ func TestToken_SpendToken(t *testing.T) { spentToken := token.SpendToken([]byte("Hello")) - if server.IsValid(spentToken, []byte("Hello World")) == true { + if server.SpendToken(spentToken, []byte("Hello World")) == nil { t.Errorf("Token Should be InValid") } - if server.IsValid(spentToken, []byte("Hello")) == false { - t.Errorf("Token Should be Valid") + if err := server.SpendToken(spentToken, []byte("Hello")); err != nil { + t.Errorf("Token Should be Valid: %v", err) } - if server.IsValid(spentToken, []byte("Hello")) == true { + if err := server.SpendToken(spentToken, []byte("Hello")); err == nil { t.Errorf("Token Should be Spent") } } func TestGenerateBlindedTokenBatch(t *testing.T) { log.SetLevel(log.LevelDebug) + db := new(persistence.BoltPersistence) + db.Open("tokens.db") server := NewTokenServer() clientTranscript := core.NewTranscript("privacyPass") serverTranscript := core.NewTranscript("privacyPass") tokens, blindedTokens := GenerateBlindedTokenBatch(10) - signedTokens, proof := server.SignBlindedTokenBatch(blindedTokens, serverTranscript) + batchProof := server.SignBlindedTokenBatch(blindedTokens, serverTranscript) - verified := UnblindSignedTokenBatch(tokens, blindedTokens, signedTokens, server.Y, proof, clientTranscript) + verified := UnblindSignedTokenBatch(tokens, blindedTokens, batchProof.SignedTokens, server.Y, batchProof.Proof, clientTranscript) if !verified { t.Errorf("Something went wrong, the proof did not pass") @@ -49,8 +52,8 @@ func TestGenerateBlindedTokenBatch(t *testing.T) { // Attempt to Spend All the tokens for _, token := range tokens { spentToken := token.SpendToken([]byte("Hello")) - if server.IsValid(spentToken, []byte("Hello")) == false { - t.Errorf("Token Should be Valid") + if err := server.SpendToken(spentToken, []byte("Hello")); err != nil { + t.Errorf("Token Should be Valid: %v", err) } } @@ -58,8 +61,9 @@ func TestGenerateBlindedTokenBatch(t *testing.T) { t.Logf("Server Transcript,: %s", serverTranscript.OutputTranscriptToAudit()) wrongTranscript := core.NewTranscript("wrongTranscript") - verified = UnblindSignedTokenBatch(tokens, blindedTokens, signedTokens, server.Y, proof, wrongTranscript) + verified = UnblindSignedTokenBatch(tokens, blindedTokens, batchProof.SignedTokens, server.Y, batchProof.Proof, wrongTranscript) if verified { t.Errorf("Something went wrong, the proof passed with wrong transcript: %s", wrongTranscript.OutputTranscriptToAudit()) } + db.Close() } diff --git a/primitives/privacypass/tokenserver.go b/primitives/privacypass/tokenserver.go index e4d1354..053400e 100644 --- a/primitives/privacypass/tokenserver.go +++ b/primitives/privacypass/tokenserver.go @@ -2,81 +2,115 @@ package privacypass import ( "crypto/hmac" + "crypto/rand" + "cwtch.im/tapir/persistence" "cwtch.im/tapir/primitives/core" "encoding/hex" "fmt" - "git.openprivacy.ca/openprivacy/libricochet-go/log" - "github.com/bwesterb/go-ristretto" + ristretto "github.com/gtank/ristretto255" "golang.org/x/crypto/sha3" "sync" ) // TokenServer implements a token server. type TokenServer struct { - k *ristretto.Scalar - Y *ristretto.Point - seen map[string]bool - mutex sync.Mutex + k *ristretto.Scalar + Y *ristretto.Element + seen map[string]bool + persistanceService persistence.Service + mutex sync.Mutex } +// SignedBatchWithProof encapsulates a signed batch of blinded tokens with a batch proof for verification +type SignedBatchWithProof struct { + SignedTokens []SignedToken + Proof DLEQProof +} + +const tokenBucket = "tokens" + // NewTokenServer generates a new TokenServer (used mostly for testing with ephemeral instances) func NewTokenServer() TokenServer { - k := new(ristretto.Scalar).Rand() - return TokenServer{k, new(ristretto.Point).ScalarMultBase(k), make(map[string]bool), sync.Mutex{}} + k := new(ristretto.Scalar) + b := make([]byte, 64) + rand.Read(b) + k.FromUniformBytes(b) + return TokenServer{k, new(ristretto.Element).ScalarBaseMult(k), make(map[string]bool), nil, sync.Mutex{}} +} + +// NewTokenServerFromStore generates a new TokenServer backed by a persistence service. +func NewTokenServerFromStore(persistenceService persistence.Service) TokenServer { + k := new(ristretto.Scalar) + b := make([]byte, 64) + rand.Read(b) + k.FromUniformBytes(b) + persistenceService.Setup([]string{tokenBucket}) + return TokenServer{k, new(ristretto.Element).ScalarBaseMult(k), make(map[string]bool), persistenceService, sync.Mutex{}} } // SignBlindedToken calculates kP for the given BlindedToken P func (ts *TokenServer) SignBlindedToken(bt BlindedToken) SignedToken { - Q := new(ristretto.Point).ScalarMult(bt.P, ts.k) + Q := new(ristretto.Element).ScalarMult(ts.k, bt.P) return SignedToken{Q} } // SignBlindedTokenBatch signs a batch of blinded tokens under a given transcript -func (ts *TokenServer) SignBlindedTokenBatch(blindedTokens []BlindedToken, transcript *core.Transcript) (signedTokens []SignedToken, proof DLEQProof) { +func (ts *TokenServer) SignBlindedTokenBatch(blindedTokens []BlindedToken, transcript *core.Transcript) SignedBatchWithProof { + var signedTokens []SignedToken for _, bt := range blindedTokens { signedTokens = append(signedTokens, ts.SignBlindedToken(bt)) } - return signedTokens, ts.constructBatchProof(blindedTokens, signedTokens, transcript) + return SignedBatchWithProof{signedTokens, ts.constructBatchProof(blindedTokens, signedTokens, transcript)} } // constructBatchProof construct a batch proof that all the signed tokens have been signed correctly func (ts *TokenServer) constructBatchProof(blindedTokens []BlindedToken, signedTokens []SignedToken, transcript *core.Transcript) DLEQProof { - transcript.AddToTranscript("X", new(ristretto.Point).SetBase().Bytes()) - transcript.AddToTranscript("Y", ts.Y.Bytes()) - transcript.AddToTranscript("P[]", []byte(fmt.Sprintf("%v", blindedTokens))) - transcript.AddToTranscript("Q[]", []byte(fmt.Sprintf("%v", signedTokens))) + transcript.NewProtocol(BatchProofProtocol) + transcript.AddToTranscript(BatchProofX, new(ristretto.Element).Base().Encode(nil)) + transcript.AddToTranscript(BatchProofY, ts.Y.Encode(nil)) + transcript.AddToTranscript(BatchProofPVector, []byte(fmt.Sprintf("%v", blindedTokens))) + transcript.AddToTranscript(BatchProofQVector, []byte(fmt.Sprintf("%v", signedTokens))) prng := transcript.CommitToPRNG("w") - M := new(ristretto.Point).SetZero() - Z := new(ristretto.Point).SetZero() + M := new(ristretto.Element).Zero() + Z := new(ristretto.Element).Zero() for i := range blindedTokens { c := prng.Next() - M = new(ristretto.Point).Add(new(ristretto.Point).ScalarMult(blindedTokens[i].P, c), M) - Z = new(ristretto.Point).Add(new(ristretto.Point).ScalarMult(signedTokens[i].Q, c), Z) + M = new(ristretto.Element).Add(new(ristretto.Element).ScalarMult(c, blindedTokens[i].P), M) + Z = new(ristretto.Element).Add(new(ristretto.Element).ScalarMult(c, signedTokens[i].Q), Z) } - return DiscreteLogEquivalenceProof(ts.k, new(ristretto.Point).SetBase(), ts.Y, M, Z, transcript) + return DiscreteLogEquivalenceProof(ts.k, new(ristretto.Element).Base(), ts.Y, M, Z, transcript) } -// IsValid returns true a SpentToken is valid and has never been spent before, false otherwise. -func (ts *TokenServer) IsValid(token SpentToken, data []byte) bool { - log.Debugf("data: [%s]", data) +// SpendToken returns true a SpentToken is valid and has never been spent before, false otherwise. +func (ts *TokenServer) SpendToken(token SpentToken, data []byte) error { ts.mutex.Lock() defer ts.mutex.Unlock() // We only want 1 client at a time redeeming tokens to prevent double-spends - if _, spent := ts.seen[hex.EncodeToString(token.T)]; spent { - return false + if ts.persistanceService == nil { + if _, spent := ts.seen[hex.EncodeToString(token.T)]; spent { + return fmt.Errorf("token: %v has already been spent", token) + } + } else { + spent, err := ts.persistanceService.Check(tokenBucket, hex.EncodeToString(token.T)) + if err != nil || spent == true { + return fmt.Errorf("token: %v has already been spent", token) + } } - Ht := sha3.Sum256(token.T) - log.Debugf("token: %x", Ht) - T := new(ristretto.Point).SetElligator(&Ht) - W := new(ristretto.Point).ScalarMult(T, ts.k) - key := sha3.Sum256(append(token.T, W.Bytes()...)) + Ht := sha3.Sum512(token.T) + T := new(ristretto.Element).FromUniformBytes(Ht[:]) + W := new(ristretto.Element).ScalarMult(ts.k, T) + key := sha3.Sum256(append(token.T, W.Encode(nil)...)) mac := hmac.New(sha3.New512, key[:]) K := mac.Sum(data) - log.Debugf("mac: \n%x\nK:%x\n", token.MAC, K) result := hmac.Equal(token.MAC, K) if result == true { - ts.seen[hex.EncodeToString(token.T)] = true + if ts.persistanceService == nil { + ts.seen[hex.EncodeToString(token.T)] = true + } else { + ts.persistanceService.Persist(tokenBucket, hex.EncodeToString(token.T), true) + } + return nil } - return result + return fmt.Errorf("token: %v is invalid and/or has not been signed by this service", token) } diff --git a/service.go b/service.go index e85333a..72645ee 100644 --- a/service.go +++ b/service.go @@ -9,7 +9,6 @@ import ( "golang.org/x/crypto/ed25519" "golang.org/x/crypto/nacl/secretbox" "io" - "net" "sync" ) @@ -19,7 +18,7 @@ type Service interface { Connect(hostname string, application Application) (bool, error) Listen(application Application) error GetConnection(connectionID string) (Connection, error) - WaitForCapabilityOrClose(connectionID string, capability string) (Connection, error) + WaitForCapabilityOrClose(connectionID string, capability Capability) (Connection, error) Shutdown() } @@ -30,19 +29,20 @@ type Connection interface { ID() *primitives.Identity Expect() []byte SetHostname(hostname string) - HasCapability(name string) bool - SetCapability(name string) + HasCapability(name Capability) bool + SetCapability(name Capability) SetEncryptionKey(key [32]byte) Send(message []byte) Close() App() Application + SetApp(application Application) IsClosed() bool } // Connection defines a Tapir Connection type connection struct { hostname string - conn net.Conn + conn io.ReadWriteCloser capabilities sync.Map encrypted bool key [32]byte @@ -54,7 +54,7 @@ type connection struct { } // NewConnection creates a new Connection -func NewConnection(id *primitives.Identity, hostname string, outbound bool, conn net.Conn, app Application) Connection { +func NewConnection(id *primitives.Identity, hostname string, outbound bool, conn io.ReadWriteCloser, app Application) Connection { connection := new(connection) connection.hostname = hostname connection.conn = conn @@ -76,6 +76,11 @@ func (c *connection) App() Application { return c.app } +// App returns the overarching application using this Connection. +func (c *connection) SetApp(application Application) { + c.app = application +} + // Hostname returns the hostname of the connection (if the connection has not been authorized it will return the // temporary hostname identifier) func (c *connection) Hostname() string { @@ -100,13 +105,13 @@ func (c *connection) SetHostname(hostname string) { } // SetCapability sets a capability on the connection -func (c *connection) SetCapability(name string) { +func (c *connection) SetCapability(name Capability) { log.Debugf("[%v -- %v] Setting Capability %v", c.identity.Hostname(), c.hostname, name) c.capabilities.Store(name, true) } // HasCapability checks if the connection has a given capability -func (c *connection) HasCapability(name string) bool { +func (c *connection) HasCapability(name Capability) bool { _, ok := c.capabilities.Load(name) return ok } @@ -142,9 +147,12 @@ func (c *connection) Expect() []byte { return []byte{} } } - len, _ := binary.Uvarint(buffer[0:2]) + length, _ := binary.Uvarint(buffer[0:2]) + if length+2 >= uint64(c.MaxLength) { + return []byte{} + } //cplog.Debugf("[%v -> %v] Wire Receive: (%d) %x", c.hostname, c.ID.Hostname(), len, buffer) - return buffer[2 : len+2] + return buffer[2 : length+2] } // SetEncryptionKey turns on application-level encryption on the connection using the given key. diff --git a/testing/tests.sh b/testing/tests.sh index c9818a1..3f79df6 100755 --- a/testing/tests.sh +++ b/testing/tests.sh @@ -3,9 +3,12 @@ set -e pwd go test ${1} -coverprofile=applications.cover.out -v ./applications +go test ${1} -coverprofile=applications.tokenboard.cover.out -v ./applications/tokenboard go test ${1} -coverprofile=primitives.cover.out -v ./primitives +go test ${1} -coverprofile=primitives.auditable.cover.out -v ./primitives/auditable go test ${1} -coverprofile=primitives.core.cover.out -v ./primitives/core go test ${1} -coverprofile=primitives.privacypass.cover.out -v ./primitives/privacypass +go test -bench "BenchmarkAuditableStore" -benchtime 1000x primitives/auditable/*.go echo "mode: set" > coverage.out && cat *.cover.out | grep -v mode: | sort -r | \ awk '{if($1 != last) {print $0;last=$1}}' >> coverage.out rm -rf *.cover.out From a19204caf4a997c5b55ed98e7e502789590fb4ce Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Tue, 26 Nov 2019 13:10:09 -0800 Subject: [PATCH 4/8] Updates given Erinns Comments --- .drone.yml | 2 +- .gitignore | 1 + applications/auth.go | 2 +- applications/token_app.go | 21 +- applications/tokenboard/client.go | 3 +- applications/tokenboard/server.go | 6 +- .../tokenboard/tokenboard_integration_test.go | 13 +- networks/tor/BaseOnionService.go | 1 - persistence/bolt_persistence.go | 1 + primitives/auditable/auditablestore.go | 2 + primitives/bloom.go | 62 ------ primitives/bloom_test.go | 24 --- primitives/core/operations.go | 186 ------------------ primitives/privacypass/dlogeq.go | 6 +- primitives/privacypass/token.go | 3 +- primitives/privacypass/token_test.go | 34 +++- primitives/privacypass/tokenserver.go | 59 ++++-- service.go | 20 +- testing/tests.sh | 12 +- 19 files changed, 138 insertions(+), 320 deletions(-) delete mode 100644 primitives/bloom.go delete mode 100644 primitives/bloom_test.go delete mode 100644 primitives/core/operations.go diff --git a/.drone.yml b/.drone.yml index a5d1d8b..496dc98 100644 --- a/.drone.yml +++ b/.drone.yml @@ -27,7 +27,7 @@ pipeline: commands: - ./tor -f ./torrc - sleep 15 - - go test -v cwtch.im/tapir/testing + - go test -race -v cwtch.im/tapir/testing notify-email: image: drillster/drone-email host: build.openprivacy.ca diff --git a/.gitignore b/.gitignore index 99c6321..e1e7b51 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ coverage.out /applications/tor/ *.db /applications/tokenboard/tor/ +fuzzing/ diff --git a/applications/auth.go b/applications/auth.go index 0a57176..ab34d42 100644 --- a/applications/auth.go +++ b/applications/auth.go @@ -63,7 +63,7 @@ func (ea *AuthApp) Init(connection tapir.Connection) { challengeRemote, _ := json.Marshal(remoteAuthMessage) challengeLocal, _ := json.Marshal(authMessage) - // Define canonical labels so both sides of the + // Define canonical labels so both sides of the connection can generate the same key var outboundAuthMessage []byte var outboundHostname string var inboundAuthMessage []byte diff --git a/applications/token_app.go b/applications/token_app.go index 1bec236..c1f17f0 100644 --- a/applications/token_app.go +++ b/applications/token_app.go @@ -18,16 +18,16 @@ type TokenApplication struct { const HasTokensCapability = tapir.Capability("HasTokensCapability") // NewInstance should always return a new instantiation of the application. -func (powapp *TokenApplication) NewInstance() tapir.Application { +func (tokenapp *TokenApplication) NewInstance() tapir.Application { app := new(TokenApplication) - app.TokenService = powapp.TokenService + app.TokenService = tokenapp.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()) +func (tokenapp *TokenApplication) Init(connection tapir.Connection) { + tokenapp.Transcript().NewProtocol("token-app") + log.Debugf(tokenapp.Transcript().OutputTranscriptToAudit()) if connection.IsOutbound() { tokens, blinded := privacypass.GenerateBlindedTokenBatch(10) data, _ := json.Marshal(blinded) @@ -35,21 +35,24 @@ func (powapp *TokenApplication) Init(connection tapir.Connection) { 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()) + verified := privacypass.UnblindSignedTokenBatch(tokens, blinded, signedBatch.SignedTokens, tokenapp.TokenService.Y, signedBatch.Proof, tokenapp.Transcript()) if verified { log.Debugf("Successfully obtained signed tokens") - powapp.Tokens = tokens + tokenapp.Tokens = tokens connection.SetCapability(HasTokensCapability) return } + // This will close the connection by default and no tokens will be available. + // This usecase can be checked by the existing WaitForCapabilityOrClose() function using the HasTokensCapability + // If the connection closes without the HasTokensCapability then the error can be handled by whatever client needs it log.Debugf("Failed to verify signed token batch") } } 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()) + batchProof := tokenapp.TokenService.SignBlindedTokenBatch(blinded, tokenapp.Transcript()) + log.Debugf(tokenapp.Transcript().OutputTranscriptToAudit()) data, _ := json.Marshal(batchProof) connection.Send(data) return diff --git a/applications/tokenboard/client.go b/applications/tokenboard/client.go index 55fa387..ba20785 100644 --- a/applications/tokenboard/client.go +++ b/applications/tokenboard/client.go @@ -60,7 +60,6 @@ func (ta *Client) Listen() { 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) @@ -101,7 +100,7 @@ func (ta *Client) PurchaseTokens() { // Post sends a Post Request to the server func (ta *Client) Post(message auditable.Message) bool { - token, err := ta.paymentHandler.NextToken(message) + token, err := ta.paymentHandler.NextToken(message, ta.connection.Hostname()) if err == nil { data, _ := json.Marshal(Message{MessageType: postRequestMessage, PostRequest: postRequest{Token: token, Message: message}}) ta.connection.Send(data) diff --git a/applications/tokenboard/server.go b/applications/tokenboard/server.go index 764f595..c279031 100644 --- a/applications/tokenboard/server.go +++ b/applications/tokenboard/server.go @@ -1,5 +1,7 @@ package tokenboard +// NOTE: This is a sketch implementation, Not suitable for production use. The real auditable store is still being designed. + import ( "cwtch.im/tapir" "cwtch.im/tapir/applications" @@ -55,7 +57,7 @@ func (ta *Server) Listen() { var message Message json.Unmarshal(data, &message) - log.Debugf("Received a Message: %v", message) + switch message.MessageType { case postRequestMessage: postrequest := message.PostRequest @@ -77,7 +79,7 @@ func (ta *Server) Listen() { } func (ta *Server) postMessageRequest(token privacypass.SpentToken, message auditable.Message) { - if err := ta.TokenService.SpendToken(token, message); err == nil { + if err := ta.TokenService.SpendToken(token, append(message, ta.connection.ID().Hostname()...)); err == nil { log.Debugf("Token is valid") signedproof := ta.AuditableStore.Add(message) data, _ := json.Marshal(Message{MessageType: postResultMessage, PostResult: postResult{true, signedproof}}) diff --git a/applications/tokenboard/tokenboard_integration_test.go b/applications/tokenboard/tokenboard_integration_test.go index ba75cb3..a16427a 100644 --- a/applications/tokenboard/tokenboard_integration_test.go +++ b/applications/tokenboard/tokenboard_integration_test.go @@ -47,23 +47,24 @@ func (fph *FreePaymentHandler) MakePayment() { ChainApplication(new(applications.ProofOfWorkApplication), applications.SuccessfulProofOfWorkCapability). ChainApplication(tokenApplication, applications.HasTokensCapability) client.Connect(fph.ServerHostname, powTokenApp) - conn,err := client.WaitForCapabilityOrClose(fph.ServerHostname, applications.HasTokensCapability) + conn, err := client.WaitForCapabilityOrClose(fph.ServerHostname, applications.HasTokensCapability) if err == nil { powtapp, _ := conn.App().(*applications.TokenApplication) fph.tokens = append(fph.tokens, powtapp.Tokens...) log.Debugf("Transcript: %v", powtapp.Transcript().OutputTranscriptToAudit()) conn.Close() + return } log.Debugf("Error making payment: %v", err) } -func (fph *FreePaymentHandler) NextToken(data []byte) (privacypass.SpentToken, error) { +func (fph *FreePaymentHandler) NextToken(data []byte, hostname string) (privacypass.SpentToken, error) { if len(fph.tokens) == 0 { return privacypass.SpentToken{}, errors.New("No more tokens") } token := fph.tokens[0] fph.tokens = fph.tokens[1:] - return token.SpendToken(data), nil + return token.SpendToken(append(data, hostname...)), nil } func TestTokenBoardApp(t *testing.T) { @@ -96,7 +97,7 @@ func TestTokenBoardApp(t *testing.T) { sg := new(sync.WaitGroup) sg.Add(1) go func() { - service.Listen(NewTokenBoardServer(&tokenService, serverAuditableStore)) + service.Listen(NewTokenBoardServer(tokenService, serverAuditableStore)) sg.Done() }() @@ -108,7 +109,7 @@ func TestTokenBoardApp(t *testing.T) { sg.Add(1) go func() { tokenApplication := new(applications.TokenApplication) - tokenApplication.TokenService = &tokenService + tokenApplication.TokenService = tokenService powTokenApp := new(applications.ApplicationChain). ChainApplication(new(applications.ProofOfWorkApplication), applications.SuccessfulProofOfWorkCapability). ChainApplication(tokenApplication, applications.HasTokensCapability) @@ -121,7 +122,7 @@ func TestTokenBoardApp(t *testing.T) { 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.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) diff --git a/networks/tor/BaseOnionService.go b/networks/tor/BaseOnionService.go index 6a401cb..6b2e620 100644 --- a/networks/tor/BaseOnionService.go +++ b/networks/tor/BaseOnionService.go @@ -54,7 +54,6 @@ func (s *BaseOnionService) WaitForCapabilityOrClose(cid string, name tapir.Capab func (s *BaseOnionService) GetConnection(hostname string) (tapir.Connection, error) { var conn tapir.Connection s.connections.Range(func(key, value interface{}) bool { - log.Debugf("Checking %v", key) connection := value.(tapir.Connection) if connection.Hostname() == hostname { if !connection.IsClosed() { diff --git a/persistence/bolt_persistence.go b/persistence/bolt_persistence.go index 5cc382f..701cc55 100644 --- a/persistence/bolt_persistence.go +++ b/persistence/bolt_persistence.go @@ -53,6 +53,7 @@ func (bp *BoltPersistence) Check(bucket string, name string) (bool, error) { val = b.Get([]byte(name)) return nil }) + if err != nil { return false, err } else if val != nil { diff --git a/primitives/auditable/auditablestore.go b/primitives/auditable/auditablestore.go index ca0b52d..8a61440 100644 --- a/primitives/auditable/auditablestore.go +++ b/primitives/auditable/auditablestore.go @@ -1,5 +1,7 @@ package auditable +// WARNING NOTE: This is a sketch implementation, Not suitable for production use. The real auditable store is still being designed. + import ( "cwtch.im/tapir/persistence" "cwtch.im/tapir/primitives" diff --git a/primitives/bloom.go b/primitives/bloom.go deleted file mode 100644 index e730f85..0000000 --- a/primitives/bloom.go +++ /dev/null @@ -1,62 +0,0 @@ -package primitives - -import ( - "crypto/sha256" - "math" - "math/big" - "sync" -) - -// BloomFilter implements a bloom filter -type BloomFilter struct { - B []bool - lock sync.Mutex -} - -// Init constructs a bloom filter of size m -func (bf *BloomFilter) Init(m int64) { - bf.B = make([]bool, m) - -} - -// Hash transforms a message to a set of bit flips -func (bf *BloomFilter) Hash(msg []byte) []int { - - // Not the fastest hash function ever, but cryptographic security is more important than speed. - hash1 := sha256.Sum256(append([]byte("h1"), msg...)) - hash2 := sha256.Sum256(append([]byte("h2"), msg...)) - hash3 := sha256.Sum256(append([]byte("h3"), msg...)) - hash4 := sha256.Sum256(append([]byte("h4"), msg...)) - - m := int64(len(bf.B)) - // Number of bytes needed to pick a position from [0,m) - B := int(math.Ceil(math.Log2(float64(m)) / 8.0)) - - p1 := big.NewInt(0).SetBytes(hash1[:B]).Int64() - p2 := big.NewInt(0).SetBytes(hash2[:B]).Int64() - p3 := big.NewInt(0).SetBytes(hash3[:B]).Int64() - p4 := big.NewInt(0).SetBytes(hash4[:B]).Int64() - - return []int{int(p1), int(p2), int(p3), int(p4)} -} - -// Insert updates the BloomFilter (suitable for concurrent use) -func (bf *BloomFilter) Insert(msg []byte) { - pos := bf.Hash(msg) - bf.lock.Lock() - defer bf.lock.Unlock() - bf.B[pos[0]] = true - bf.B[pos[1]] = true - bf.B[pos[2]] = true - bf.B[pos[3]] = true -} - -// Check returns true if the messages might be in the BloomFilter -// (No false positives, possible false negatives due to the probabilistic nature of the filter) -func (bf *BloomFilter) Check(msg []byte) bool { - pos := bf.Hash(msg) - if bf.B[pos[0]] && bf.B[pos[1]] && bf.B[pos[2]] && bf.B[pos[3]] { - return true - } - return false -} diff --git a/primitives/bloom_test.go b/primitives/bloom_test.go deleted file mode 100644 index ecda1ed..0000000 --- a/primitives/bloom_test.go +++ /dev/null @@ -1,24 +0,0 @@ -package primitives - -import ( - "strconv" - "testing" -) - -func TestBloomFilter_Insert(t *testing.T) { - bf := new(BloomFilter) - bf.Init(256) - - fp := 0 - for i := 0; i < 256; i++ { - input := []byte("test" + strconv.Itoa(256+i)) - if bf.Check(input) { - t.Log("False Positive!") - fp++ - } - bf.Insert(input) - } - - t.Logf("Num false positives %v %v%%", fp, (float64(fp)/256.0)*100) - -} diff --git a/primitives/core/operations.go b/primitives/core/operations.go deleted file mode 100644 index 61c3bdd..0000000 --- a/primitives/core/operations.go +++ /dev/null @@ -1,186 +0,0 @@ -package core - -import ( - "fmt" - ristretto "github.com/gtank/ristretto255" -) - -// ScalarVector explicit type checking -type ScalarVector []*ristretto.Scalar - -// PointVector explicit type checking -type PointVector []*ristretto.Element - -// GeneratorVector explicit type checking -type GeneratorVector []*ristretto.Element - -// CopyVector safely copies a vector -func CopyVector(G GeneratorVector) GeneratorVector { - H := make(GeneratorVector, len(G)) - for i, g := range G { - H[i] = new(ristretto.Element).Add(new(ristretto.Element).Zero(), g) - } - return H -} - -// InnerProduct takes the inner product of a and b i.e. -func InnerProduct(a, b ScalarVector) *ristretto.Scalar { - if len(a) != len(b) { - panic(fmt.Sprintf("len(a) = %v ; len(b) = %v;", len(a), len(b))) - } - - result := new(ristretto.Scalar).Zero() - for i, ai := range a { - result.Add(result, new(ristretto.Scalar).Multiply(ai, b[i])) - } - return result -} - -// MultiExp takes in a vector of scalars = {a,b,c...} and a vector of generator = {A,B,C...} and outputs -// {aA,bB,cC} -func MultiExp(a ScalarVector, G GeneratorVector) *ristretto.Element { - if len(a) > len(G) { - panic(fmt.Sprintf("len(a) = %v ; len(b) = %v;", len(a), len(G))) - } - result := new(ristretto.Element).Zero() - for i, ai := range a { - aG := new(ristretto.Element).ScalarMult(ai, G[i]) - result = new(ristretto.Element).Add(result, aG) - } - return result -} - -// SafeAppend is defined for a vector of Scalars -func (a ScalarVector) SafeAppend(b *ristretto.Scalar) ScalarVector { - list := make(ScalarVector, len(a)+1) - for i := 0; i < len(a); i++ { - list[i] = a[i] - } - list[len(a)] = b - return list -} - -// Join is defined for a vector of Scalars -func (a ScalarVector) Join(b ScalarVector) ScalarVector { - list := make(ScalarVector, len(a)+len(b)) - for i := 0; i < len(a); i++ { - list[i] = a[i] - } - for i := len(a); i < len(a)+len(b); i++ { - list[i] = b[i-len(a)] - } - return list -} - -// SafeAppend as defined for a vector of Generators -func (a GeneratorVector) SafeAppend(b *ristretto.Element) GeneratorVector { - list := make(GeneratorVector, len(a)+1) - for i := 0; i < len(a); i++ { - list[i] = a[i] - } - list[len(a)] = b - return list -} - -// Join as defined for a vector of Generators -func (a GeneratorVector) Join(b GeneratorVector) GeneratorVector { - list := make(GeneratorVector, len(a)+len(b)) - for i := 0; i < len(a); i++ { - list[i] = a[i] - } - for i := len(a); i < len(a)+len(b); i++ { - list[i] = b[i-len(a)] - } - return list -} - -// VectorAddScalar takes in a vector v = {a,b,c..} and a scalar s and outputs {a+s,b+s,c+s....} -func VectorAddScalar(vector ScalarVector, scalar *ristretto.Scalar) ScalarVector { - result := make(ScalarVector, len(vector)) - for i := range vector { - result[i] = new(ristretto.Scalar) - result[i].Add(vector[i], scalar) - } - return result -} - -// VectorNegate takes in a vector v = {a,b,c..} and a scalar s and outputs {-a,-b,-c} -func VectorNegate(vector ScalarVector) ScalarVector { - result := make(ScalarVector, len(vector)) - for i := range vector { - result[i] = new(ristretto.Scalar).Negate(vector[i]) - } - return result -} - -// VectorMulScalar takes in a vector v = {a,b,c..} and a scalar s and outputs {as,bs,cs....} -func VectorMulScalar(vector ScalarVector, scalar *ristretto.Scalar) ScalarVector { - result := make(ScalarVector, len(vector)) - for i := range vector { - result[i] = new(ristretto.Scalar) - result[i].Multiply(vector[i], scalar) - } - return result -} - -// EntrywiseSum takes the entry wise sum of two vectors -func EntrywiseSum(vector ScalarVector, vector2 ScalarVector) ScalarVector { - result := make(ScalarVector, len(vector)) - for i, v := range vector { - result[i] = new(ristretto.Scalar) - result[i].Add(v, vector2[i]) - } - return result -} - -// EntrywiseSub takes the entry wise subtraction of two vectors -func EntrywiseSub(vector ScalarVector, vector2 ScalarVector) ScalarVector { - result := make(ScalarVector, len(vector)) - for i, v := range vector { - result[i] = new(ristretto.Scalar) - result[i].Subtract(v, vector2[i]) - } - return result -} - -// EntryWiseProduct takes the entry wise product of two vectors -func EntryWiseProduct(vector ScalarVector, vector2 ScalarVector) ScalarVector { - result := make(ScalarVector, len(vector)) - for i, v := range vector { - result[i] = new(ristretto.Scalar) - result[i].Multiply(v, vector2[i]) - } - return result -} - -// One returns a ristretto scalar == 1 -func One() *ristretto.Scalar { - one := new(ristretto.Scalar) - one.Decode([]byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}) - return one -} - -// IdentityVector is a convenience function to generate a vector v = {1,1,1...1} -func IdentityVector(n int) ScalarVector { - result := make(ScalarVector, n) - one := new(ristretto.Scalar) - one.Decode([]byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}) - for i := 0; i < n; i++ { - result[i] = one - } - return result -} - -// PowerVector creates a vector v = {1,x,x^2,x^3..x^n} -func PowerVector(x *ristretto.Scalar, n int) ScalarVector { - result := make(ScalarVector, n) - one := new(ristretto.Scalar) - one.Decode([]byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}) - result[0] = one - result[1] = x - for i := 2; i < n; i++ { - result[i] = new(ristretto.Scalar) - result[i].Multiply(result[i-1], x) - } - return result -} diff --git a/primitives/privacypass/dlogeq.go b/primitives/privacypass/dlogeq.go index e4108ab..c57dafc 100644 --- a/primitives/privacypass/dlogeq.go +++ b/primitives/privacypass/dlogeq.go @@ -14,7 +14,7 @@ type DLEQProof struct { } // DiscreteLogEquivalenceProof constructs a valid DLEQProof for the given parameters and transcript -// Given P = kX, Q = kP, Y=kX +// Given Y = kX & Q = kP // Peggy: t := choose randomly from Zq // A := tX // B := tP @@ -43,12 +43,12 @@ func DiscreteLogEquivalenceProof(k *ristretto.Scalar, X *ristretto.Element, Y *r } // VerifyDiscreteLogEquivalenceProof verifies the DLEQ for the given parameters and transcript -// Given P = kX, Q = kP, Y=kX, and Proof = (c,s) +// Given Y = kX & Q = kP and Proof = (c,s) // Vicky: X' := sX // Y' := cY // P' := sP // Q' := cQ -// A' = X'+Y' == sX + cY ?= sX + ckX == (s+ck)X == tX == A +// A' = X'+Y' == sX + cY ?= sG + ckG == (s+ck)X == tX == A // B' = P'+Q' == sP + cQ ?= sP + ckP == (s+ck)P == tP == B // c' := H(transcript(X,Y,P,Q,A',B')) // Tests c ?= c diff --git a/primitives/privacypass/token.go b/primitives/privacypass/token.go index 4dc8829..04de7ae 100644 --- a/primitives/privacypass/token.go +++ b/primitives/privacypass/token.go @@ -38,7 +38,8 @@ type SpentToken struct { // TokenPaymentHandler defines an interface with external payment processors type TokenPaymentHandler interface { MakePayment() - NextToken(data []byte) (SpentToken, error) + // Next Token + NextToken(data []byte, hostname string) (SpentToken, error) } // GenBlindedToken initializes the Token diff --git a/primitives/privacypass/token_test.go b/primitives/privacypass/token_test.go index 9a23596..18b68bb 100644 --- a/primitives/privacypass/token_test.go +++ b/primitives/privacypass/token_test.go @@ -4,6 +4,8 @@ import ( "cwtch.im/tapir/persistence" "cwtch.im/tapir/primitives/core" "git.openprivacy.ca/openprivacy/libricochet-go/log" + "github.com/gtank/ristretto255" + "golang.org/x/crypto/sha3" "testing" ) @@ -31,11 +33,40 @@ func TestToken_SpendToken(t *testing.T) { } } +func TestToken_ConstrainToToken(t *testing.T) { + server := NewTokenServer() + + token := new(Token) + blindedToken := token.GenBlindedToken() + + signedToken := server.SignBlindedToken(blindedToken) + token.unblindSignedToken(signedToken) + + spentToken := token.SpendToken([]byte("Hello")) + + if server.SpendToken(spentToken, []byte("Hello World")) == nil { + t.Errorf("Token Should be InValid") + } + + token2 := new(Token) + blindedToken2 := token2.GenBlindedToken() + Ht := sha3.Sum512(token.t) + T := new(ristretto255.Element).FromUniformBytes(Ht[:]) + // Constraint forces T = kW to be part of the batch proof + // And because the batch proof must prove that *all* inputs share the same key and also checks the servers public key + // We get a consistency check for almost free. + signedTokens := server.SignBlindedTokenBatchWithConstraint([]BlindedToken{blindedToken2}, *token, core.NewTranscript("")) + transcript := core.NewTranscript("") + t.Logf("Result of constaint proof %v", UnblindSignedTokenBatch([]*Token{token2}, []BlindedToken{blindedToken2, {P: T}}, append(signedTokens.SignedTokens, SignedToken{token.W}), server.Y, signedTokens.Proof, transcript)) + t.Log(transcript.OutputTranscriptToAudit()) +} + func TestGenerateBlindedTokenBatch(t *testing.T) { log.SetLevel(log.LevelDebug) db := new(persistence.BoltPersistence) db.Open("tokens.db") - server := NewTokenServer() + defer db.Close() + server := NewTokenServerFromStore(db) clientTranscript := core.NewTranscript("privacyPass") serverTranscript := core.NewTranscript("privacyPass") @@ -65,5 +96,4 @@ func TestGenerateBlindedTokenBatch(t *testing.T) { if verified { t.Errorf("Something went wrong, the proof passed with wrong transcript: %s", wrongTranscript.OutputTranscriptToAudit()) } - db.Close() } diff --git a/primitives/privacypass/tokenserver.go b/primitives/privacypass/tokenserver.go index 053400e..a4f32a7 100644 --- a/primitives/privacypass/tokenserver.go +++ b/primitives/privacypass/tokenserver.go @@ -23,29 +23,48 @@ type TokenServer struct { // SignedBatchWithProof encapsulates a signed batch of blinded tokens with a batch proof for verification type SignedBatchWithProof struct { - SignedTokens []SignedToken - Proof DLEQProof + SignedTokens []SignedToken `json:"st"` + Proof DLEQProof `json:"dp"` } const tokenBucket = "tokens" +const keyBucket = "keys" // NewTokenServer generates a new TokenServer (used mostly for testing with ephemeral instances) -func NewTokenServer() TokenServer { +func NewTokenServer() *TokenServer { k := new(ristretto.Scalar) b := make([]byte, 64) - rand.Read(b) + _, err := rand.Read(b) + if err != nil { + // unable to generate secure random numbers + panic("unable to generate secure random numbers") + } k.FromUniformBytes(b) - return TokenServer{k, new(ristretto.Element).ScalarBaseMult(k), make(map[string]bool), nil, sync.Mutex{}} + 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) +func NewTokenServerFromStore(persistenceService persistence.Service) *TokenServer { + tokenServer := NewTokenServer() persistenceService.Setup([]string{tokenBucket}) - return TokenServer{k, new(ristretto.Element).ScalarBaseMult(k), make(map[string]bool), persistenceService, sync.Mutex{}} + persistenceService.Setup([]string{keyBucket}) + exists, err := persistenceService.Check(keyBucket, "k") + if err != nil { + panic(err) + } + // if we don't have a stored k then save the one we have generated + // otherwise use the k we have stored + if !exists { + persistenceService.Persist(keyBucket, "k", tokenServer.k) + } else { + persistenceService.Load(keyBucket, "k", tokenServer.k) + + // recalculate public key from stored k + tokenServer.Y = new(ristretto.Element).ScalarBaseMult(tokenServer.k) + } + + tokenServer.persistanceService = persistenceService + return tokenServer } // SignBlindedToken calculates kP for the given BlindedToken P @@ -63,6 +82,20 @@ func (ts *TokenServer) SignBlindedTokenBatch(blindedTokens []BlindedToken, trans return SignedBatchWithProof{signedTokens, ts.constructBatchProof(blindedTokens, signedTokens, transcript)} } +// SignBlindedTokenBatchWithConstraint signs a batch of blinded tokens under a given transcript given a contraint that the tokens must be signed +// by the same public key as an existing token +func (ts *TokenServer) SignBlindedTokenBatchWithConstraint(blindedTokens []BlindedToken, token Token, transcript *core.Transcript) SignedBatchWithProof { + var signedTokens []SignedToken + for _, bt := range blindedTokens { + signedTokens = append(signedTokens, ts.SignBlindedToken(bt)) + } + Ht := sha3.Sum512(token.t) + T := new(ristretto.Element).FromUniformBytes(Ht[:]) + // W == kT + blindedTokens = append(blindedTokens, BlindedToken{P: T}) + return SignedBatchWithProof{signedTokens, ts.constructBatchProof(blindedTokens, append(signedTokens, SignedToken{Q: token.W}), transcript)} +} + // constructBatchProof construct a batch proof that all the signed tokens have been signed correctly func (ts *TokenServer) constructBatchProof(blindedTokens []BlindedToken, signedTokens []SignedToken, transcript *core.Transcript) DLEQProof { transcript.NewProtocol(BatchProofProtocol) @@ -102,8 +135,8 @@ func (ts *TokenServer) SpendToken(token SpentToken, data []byte) error { 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) - result := hmac.Equal(token.MAC, K) + computedMAC := mac.Sum(data) + result := hmac.Equal(token.MAC, computedMAC) if result == true { if ts.persistanceService == nil { ts.seen[hex.EncodeToString(token.T)] = true diff --git a/service.go b/service.go index 72645ee..bc2a6bd 100644 --- a/service.go +++ b/service.go @@ -51,6 +51,7 @@ type connection struct { outbound bool closed bool MaxLength int + lock sync.Mutex } // NewConnection creates a new Connection @@ -73,17 +74,23 @@ func (c *connection) ID() *primitives.Identity { // App returns the overarching application using this Connection. func (c *connection) App() Application { + c.lock.Lock() + defer c.lock.Unlock() return c.app } // App returns the overarching application using this Connection. func (c *connection) SetApp(application Application) { + c.lock.Lock() + defer c.lock.Unlock() c.app = application } // Hostname returns the hostname of the connection (if the connection has not been authorized it will return the // temporary hostname identifier) func (c *connection) Hostname() string { + c.lock.Lock() + defer c.lock.Unlock() return c.hostname } @@ -95,11 +102,15 @@ func (c *connection) IsOutbound() bool { // IsClosed returns true if the connection is closed (connections cannot be reopened) func (c *connection) IsClosed() bool { + c.lock.Lock() + defer c.lock.Unlock() return c.closed } // SetHostname sets the hostname on the connection func (c *connection) SetHostname(hostname string) { + c.lock.Lock() + defer c.lock.Unlock() log.Debugf("[%v -- %v] Asserting Remote Hostname: %v", c.identity.Hostname(), c.hostname, hostname) c.hostname = hostname } @@ -118,6 +129,8 @@ func (c *connection) HasCapability(name Capability) bool { // Close forcibly closes the connection func (c *connection) Close() { + c.lock.Lock() + defer c.lock.Unlock() c.closed = true c.conn.Close() } @@ -133,7 +146,8 @@ func (c *connection) Expect() []byte { c.closed = true return []byte{} } - + c.lock.Lock() + defer c.lock.Unlock() if c.encrypted { var decryptNonce [24]byte copy(decryptNonce[:], buffer[:24]) @@ -157,6 +171,8 @@ func (c *connection) Expect() []byte { // SetEncryptionKey turns on application-level encryption on the connection using the given key. func (c *connection) SetEncryptionKey(key [32]byte) { + c.lock.Lock() + defer c.lock.Unlock() c.key = key c.encrypted = true } @@ -168,6 +184,8 @@ func (c *connection) Send(message []byte) { binary.PutUvarint(buffer[0:2], uint64(len(message))) copy(buffer[2:], message) + c.lock.Lock() + defer c.lock.Unlock() if c.encrypted { var nonce [24]byte if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil { diff --git a/testing/tests.sh b/testing/tests.sh index 3f79df6..f402dbe 100755 --- a/testing/tests.sh +++ b/testing/tests.sh @@ -2,12 +2,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 -race ${1} -coverprofile=applications.cover.out -v ./applications +go test -race ${1} -coverprofile=applications.tokenboard.cover.out -v ./applications/tokenboard +go test -race ${1} -coverprofile=primitives.cover.out -v ./primitives +go test -race ${1} -coverprofile=primitives.auditable.cover.out -v ./primitives/auditable +go test -race ${1} -coverprofile=primitives.core.cover.out -v ./primitives/core +go test -race ${1} -coverprofile=primitives.privacypass.cover.out -v ./primitives/privacypass go test -bench "BenchmarkAuditableStore" -benchtime 1000x primitives/auditable/*.go echo "mode: set" > coverage.out && cat *.cover.out | grep -v mode: | sort -r | \ awk '{if($1 != last) {print $0;last=$1}}' >> coverage.out From a0e58b8736e2e7967b91d39c3cfa7bd0e345a643 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Wed, 27 Nov 2019 15:15:33 -0800 Subject: [PATCH 5/8] Update per Dans' Comments --- applications/token_app.go | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/applications/token_app.go b/applications/token_app.go index c1f17f0..f46fc2a 100644 --- a/applications/token_app.go +++ b/applications/token_app.go @@ -16,6 +16,7 @@ type TokenApplication struct { // HasTokensCapability is granted once the client has obtained signed tokens const HasTokensCapability = tapir.Capability("HasTokensCapability") +const numTokens = 10 // NewInstance should always return a new instantiation of the application. func (tokenapp *TokenApplication) NewInstance() tapir.Application { @@ -29,7 +30,7 @@ func (tokenapp *TokenApplication) Init(connection tapir.Connection) { tokenapp.Transcript().NewProtocol("token-app") log.Debugf(tokenapp.Transcript().OutputTranscriptToAudit()) if connection.IsOutbound() { - tokens, blinded := privacypass.GenerateBlindedTokenBatch(10) + tokens, blinded := privacypass.GenerateBlindedTokenBatch(numTokens) data, _ := json.Marshal(blinded) connection.Send(data) var signedBatch privacypass.SignedBatchWithProof @@ -47,15 +48,17 @@ func (tokenapp *TokenApplication) Init(connection tapir.Connection) { // If the connection closes without the HasTokensCapability then the error can be handled by whatever client needs it log.Debugf("Failed to verify signed token batch") } - } else { - var blinded []privacypass.BlindedToken - err := json.Unmarshal(connection.Expect(), &blinded) - if err == nil { - batchProof := tokenapp.TokenService.SignBlindedTokenBatch(blinded, tokenapp.Transcript()) - log.Debugf(tokenapp.Transcript().OutputTranscriptToAudit()) - data, _ := json.Marshal(batchProof) - connection.Send(data) - return - } + return + } + + // We are the server + var blinded []privacypass.BlindedToken + err := json.Unmarshal(connection.Expect(), &blinded) + if err == nil { + batchProof := tokenapp.TokenService.SignBlindedTokenBatch(blinded, tokenapp.Transcript()) + log.Debugf(tokenapp.Transcript().OutputTranscriptToAudit()) + data, _ := json.Marshal(batchProof) + connection.Send(data) + return } } From dcf0635f5f21d9f8399fc8abbfd250b726160011 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Fri, 29 Nov 2019 13:34:44 -0800 Subject: [PATCH 6/8] Fixing Versions --- go.mod | 4 ++-- go.sum | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index ecb300a..5d956ee 100644 --- a/go.mod +++ b/go.mod @@ -2,8 +2,8 @@ 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 + github.com/gtank/merlin v0.1.1 + github.com/gtank/ristretto255 v0.1.1 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 diff --git a/go.sum b/go.sum index d518b0a..b4df8f1 100644 --- a/go.sum +++ b/go.sum @@ -10,10 +10,14 @@ 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/merlin v0.1.1 h1:eQ90iG7K9pOhtereWsmyRJ6RAwcP4tHTDBHXNg+u5is= +github.com/gtank/merlin v0.1.1/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s= github.com/gtank/ristretto255 v0.1.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/gtank/ristretto255 v0.1.1 h1:A+VVUhf73TS5HRfCnfMBqTBujkbwY3Fo8sRSFvL3cIg= +github.com/gtank/ristretto255 v0.1.1/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= From c5e3837a5d0baf6ad26db1134bf8b4cba30f5819 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Mon, 2 Dec 2019 12:37:25 -0800 Subject: [PATCH 7/8] Amend sign with token constraint api to use []byte instead of token --- primitives/privacypass/token.go | 8 ++++++-- primitives/privacypass/token_test.go | 4 +++- primitives/privacypass/tokenserver.go | 9 +++++---- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/primitives/privacypass/token.go b/primitives/privacypass/token.go index 04de7ae..70ac785 100644 --- a/primitives/privacypass/token.go +++ b/primitives/privacypass/token.go @@ -19,6 +19,11 @@ type Token struct { W *ristretto.Element } +// GetT returns the underlying bytes for token for use in constraint proofs. +func (t Token) GetT() []byte { + return t.t +} + // BlindedToken encapsulates a Blinded Token type BlindedToken struct { P *ristretto.Element @@ -53,7 +58,6 @@ func (t *Token) GenBlindedToken() BlindedToken { t.r.FromUniformBytes(b) Ht := sha3.Sum512(t.t) - log.Debugf("token: %x", Ht) T := new(ristretto.Element).FromUniformBytes(Ht[:]) P := new(ristretto.Element).ScalarMult(t.r, T) return BlindedToken{P} @@ -103,7 +107,7 @@ func verifyBatchProof(dleq DLEQProof, Y *ristretto.Element, blindedTokens []Blin 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()) + log.Debugf("Failed to unblind tokens: %v", transcript.OutputTranscriptToAudit()) return false } for i, t := range tokens { diff --git a/primitives/privacypass/token_test.go b/primitives/privacypass/token_test.go index 18b68bb..cd15b09 100644 --- a/primitives/privacypass/token_test.go +++ b/primitives/privacypass/token_test.go @@ -55,8 +55,10 @@ func TestToken_ConstrainToToken(t *testing.T) { // Constraint forces T = kW to be part of the batch proof // And because the batch proof must prove that *all* inputs share the same key and also checks the servers public key // We get a consistency check for almost free. - signedTokens := server.SignBlindedTokenBatchWithConstraint([]BlindedToken{blindedToken2}, *token, core.NewTranscript("")) + signedTokens := server.SignBlindedTokenBatchWithConstraint([]BlindedToken{blindedToken2}, token.t, core.NewTranscript("")) transcript := core.NewTranscript("") + + // NOTE: For this to work token.t and token.W need to be obtain by the client from known source e.g. a public message board. t.Logf("Result of constaint proof %v", UnblindSignedTokenBatch([]*Token{token2}, []BlindedToken{blindedToken2, {P: T}}, append(signedTokens.SignedTokens, SignedToken{token.W}), server.Y, signedTokens.Proof, transcript)) t.Log(transcript.OutputTranscriptToAudit()) } diff --git a/primitives/privacypass/tokenserver.go b/primitives/privacypass/tokenserver.go index a4f32a7..c5036d2 100644 --- a/primitives/privacypass/tokenserver.go +++ b/primitives/privacypass/tokenserver.go @@ -82,18 +82,19 @@ func (ts *TokenServer) SignBlindedTokenBatch(blindedTokens []BlindedToken, trans return SignedBatchWithProof{signedTokens, ts.constructBatchProof(blindedTokens, signedTokens, transcript)} } -// SignBlindedTokenBatchWithConstraint signs a batch of blinded tokens under a given transcript given a contraint that the tokens must be signed +// SignBlindedTokenBatchWithConstraint signs a batch of blinded tokens under a given transcript given a constraint that the tokens must be signed // by the same public key as an existing token -func (ts *TokenServer) SignBlindedTokenBatchWithConstraint(blindedTokens []BlindedToken, token Token, transcript *core.Transcript) SignedBatchWithProof { +func (ts *TokenServer) SignBlindedTokenBatchWithConstraint(blindedTokens []BlindedToken, constraintToken []byte, transcript *core.Transcript) SignedBatchWithProof { var signedTokens []SignedToken for _, bt := range blindedTokens { signedTokens = append(signedTokens, ts.SignBlindedToken(bt)) } - Ht := sha3.Sum512(token.t) + Ht := sha3.Sum512(constraintToken) T := new(ristretto.Element).FromUniformBytes(Ht[:]) // W == kT + W := new(ristretto.Element).ScalarMult(ts.k, T) blindedTokens = append(blindedTokens, BlindedToken{P: T}) - return SignedBatchWithProof{signedTokens, ts.constructBatchProof(blindedTokens, append(signedTokens, SignedToken{Q: token.W}), transcript)} + return SignedBatchWithProof{signedTokens, ts.constructBatchProof(blindedTokens, append(signedTokens, SignedToken{Q: W}), transcript)} } // constructBatchProof construct a batch proof that all the signed tokens have been signed correctly From b2975e72245ec4cf21549f48f9aa31d1574822fb Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Tue, 17 Dec 2019 14:28:34 -0800 Subject: [PATCH 8/8] Updating ristretto dependency --- go.mod | 2 +- go.sum | 10 ++-------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 5d956ee..e2a0995 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module cwtch.im/tapir require ( git.openprivacy.ca/openprivacy/libricochet-go v1.0.4 github.com/gtank/merlin v0.1.1 - github.com/gtank/ristretto255 v0.1.1 + github.com/gtank/ristretto255 v0.1.2 go.etcd.io/bbolt v1.3.3 golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f golang.org/x/sync v0.0.0-20190423024810-112230192c58 // indirect diff --git a/go.sum b/go.sum index b4df8f1..c5a1a2a 100644 --- a/go.sum +++ b/go.sum @@ -8,16 +8,10 @@ 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/merlin v0.1.1 h1:eQ90iG7K9pOhtereWsmyRJ6RAwcP4tHTDBHXNg+u5is= github.com/gtank/merlin v0.1.1/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s= -github.com/gtank/ristretto255 v0.1.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/gtank/ristretto255 v0.1.1 h1:A+VVUhf73TS5HRfCnfMBqTBujkbwY3Fo8sRSFvL3cIg= -github.com/gtank/ristretto255 v0.1.1/go.mod h1:Ph5OpO6c7xKUGROZfWVLiJf9icMDwUeIvY4OmlYW69o= +github.com/gtank/ristretto255 v0.1.2 h1:JEqUCPA1NvLq5DwYtuzigd7ss8fwbYay9fi4/5uMzcc= +github.com/gtank/ristretto255 v0.1.2/go.mod h1:Ph5OpO6c7xKUGROZfWVLiJf9icMDwUeIvY4OmlYW69o= github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643 h1:hLDRPB66XQT/8+wG9WsDpiCvZf1yKO7sz7scAjSlBa0= github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643/go.mod h1:43+3pMjjKimDBf5Kr4ZFNGbLql1zKkbImw+fZbw3geM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=