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