First cut of Token Board

This commit is contained in:
Sarah Jamie Lewis 2019-09-14 22:50:06 -07:00
parent 5b64c2d708
commit 345d11f506
8 changed files with 389 additions and 43 deletions

1
.gitignore vendored
View File

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

192
applications/tokenboard.go Normal file
View File

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

View File

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

View File

@ -1,22 +1,25 @@
package primitives package primitives
import ( import (
"crypto/subtle"
"cwtch.im/tapir/primitives/core" "cwtch.im/tapir/primitives/core"
"encoding/base64" "encoding/base64"
"errors" "errors"
"golang.org/x/crypto/ed25519" "golang.org/x/crypto/ed25519"
"sync"
) )
// SignedProof encapsulates a signed proof // SignedProof encapsulates a signed proof
type SignedProof []byte type SignedProof struct {
Commit []byte
Proof []byte
}
// Message encapsulates a message for more readable code. // Message encapsulates a message for more readable code.
type Message []byte type Message []byte
// State defines an array of messages. // State defines an array of messages.
type State struct { type State struct {
message []Message Messages []Message
} }
// AuditableStore defines a cryptographically secure & auditable transcript of messages sent from multiple // AuditableStore defines a cryptographically secure & auditable transcript of messages sent from multiple
@ -25,50 +28,64 @@ type AuditableStore struct {
state State state State
identity Identity identity Identity
transcript *core.Transcript transcript *core.Transcript
latestCommit []byte LatestCommit []byte
commits map[string]bool commits map[string]int
mutex sync.Mutex
} }
// Init initializes an auditable store // Init initializes an auditable store
func (as *AuditableStore) Init(identity Identity) { func (as *AuditableStore) Init(identity Identity) {
as.identity = identity as.identity = identity
as.transcript = core.NewTranscript("auditable-data-store") 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 // Add adds a message to the auditable store
func (as *AuditableStore) Add(message Message, latestCommit []byte) ([]byte, SignedProof, error) { func (as *AuditableStore) Add(message Message) SignedProof {
if subtle.ConstantTimeCompare(latestCommit, as.latestCommit) == 1 { as.mutex.Lock()
as.state.message = append(as.state.message, message) defer as.mutex.Unlock()
as.transcript.AddToTranscript("new-message", message) as.state.Messages = append(as.state.Messages, message)
as.latestCommit = as.identity.Sign(as.transcript.CommitToTranscript("commit")) as.transcript.AddToTranscript("new-message", message)
return as.latestCommit, as.identity.Sign(as.latestCommit), nil as.LatestCommit = as.identity.Sign(as.transcript.CommitToTranscript("commit"))
} return SignedProof{as.LatestCommit, as.identity.Sign(as.LatestCommit)}
// 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 // GetState returns the current auditable state
func (as *AuditableStore) GetState() (State, []byte, SignedProof) { func (as *AuditableStore) GetState() (State, SignedProof) {
return as.state, as.latestCommit, as.identity.Sign(as.latestCommit) 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 // 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 { func (as *AuditableStore) MergeState(state State, signedStateProof SignedProof) error {
next := len(as.state.message) next := len(as.state.Messages)
for _, m := range state.message[next:] { for i, m := range state.Messages[next:] {
as.state.message = append(as.state.message, m) as.state.Messages = append(as.state.Messages, m)
// We reconstruct the transcript // We reconstruct the transcript
as.transcript.AddToTranscript("new-message", m) as.transcript.AddToTranscript("new-message", m)
as.latestCommit = as.identity.Sign(as.transcript.CommitToTranscript("commit")) as.LatestCommit = as.identity.Sign(as.transcript.CommitToTranscript("commit"))
as.commits[base64.StdEncoding.EncodeToString(as.latestCommit)] = true as.commits[base64.StdEncoding.EncodeToString(as.LatestCommit)] = next + i
} }
// verify that our state matches the servers signed state // verify that our state matches the servers signed state
// this is *not* a security check, as a rogue server can simply sign any state // this is *not* a security check, as a rogue server can simply sign any state
// however committing to a state allows us to build fraud proofs for malicious servers later on. // however committing to a state allows us to build fraud proofs for malicious servers later on.
if ed25519.Verify(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 errors.New("state is not consistent, the server is malicious")
} }
return nil 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. // If, after syncing, the FraudProof still validates, then the server must be malicious.
// the information revealed by publicizing a fraud proof is minimal it only reveals the inconsistent transcript commit // the information revealed by publicizing a fraud proof is minimal it only reveals the inconsistent transcript commit
// and not the cause (which could be reordered messages, dropped messages, additional messages or any combination) // and not the cause (which could be reordered messages, dropped messages, additional messages or any combination)
func (as *AuditableStore) VerifyFraudProof(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 due to misuse of this function (trying to verify a proof with the wrong public key)
// This could happen if the server lies to us and submits a fake state proof, however we cannot use this to // This could happen if the server lies to us and submits a fake state proof, however we cannot use this to
// prove that the server is acting maliciously // prove that the server is acting maliciously
return false, errors.New("signed proof has not been signed by the given public key") return false, errors.New("signed proof has not been signed by the given public key")
} }
_, exists := as.commits[base64.StdEncoding.EncodeToString(commit)] _, exists := as.commits[base64.StdEncoding.EncodeToString(signedFraudProof.Commit)]
if !exists { if !exists {
// We have a message signed by the server which verifies that a message was inserted into the state at a given index // We have a message signed by the server which verifies that a message was inserted into the state at a given index
// However this directly contradicts our version of the state. // However this directly contradicts our version of the state.

View File

@ -12,22 +12,22 @@ func TestAuditableStore(t *testing.T) {
as.Init(serverID) as.Init(serverID)
vs.Init(serverID) // This doesn't do anything vs.Init(serverID) // This doesn't do anything
as.Add([]byte("Hello World"), as.latestCommit) as.Add([]byte("Hello World"))
state, _, proof := as.GetState() 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") 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. // If you comment these out it simulates a lying server.
state, _, proof = as.GetState() 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") 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 { if err != nil {
t.Fatalf("Error validated fraud proof: %v", err) t.Fatalf("Error validated fraud proof: %v", err)

View File

@ -5,6 +5,7 @@ import (
"crypto/rand" "crypto/rand"
"cwtch.im/tapir/primitives/core" "cwtch.im/tapir/primitives/core"
"fmt" "fmt"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"github.com/bwesterb/go-ristretto" "github.com/bwesterb/go-ristretto"
"golang.org/x/crypto/sha3" "golang.org/x/crypto/sha3"
) )
@ -29,10 +30,16 @@ type SignedToken struct {
// SpentToken encapsulates the parameters needed to spend a Token // SpentToken encapsulates the parameters needed to spend a Token
type SpentToken struct { type SpentToken struct {
t []byte T []byte
MAC []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 // GenBlindedToken initializes the Token
// GenToken() & Blind() // GenToken() & Blind()
func (t *Token) GenBlindedToken() BlindedToken { func (t *Token) GenBlindedToken() BlindedToken {
@ -41,6 +48,7 @@ func (t *Token) GenBlindedToken() BlindedToken {
t.r = new(ristretto.Scalar).Rand() t.r = new(ristretto.Scalar).Rand()
Ht := sha3.Sum256(t.t) Ht := sha3.Sum256(t.t)
log.Debugf("token: %x", Ht)
T := new(ristretto.Point).SetElligator(&Ht) T := new(ristretto.Point).SetElligator(&Ht)
P := new(ristretto.Point).ScalarMult(T, t.r) P := new(ristretto.Point).ScalarMult(T, t.r)
return BlindedToken{P} return BlindedToken{P}

View File

@ -2,6 +2,7 @@ package privacypass
import ( import (
"cwtch.im/tapir/primitives/core" "cwtch.im/tapir/primitives/core"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"testing" "testing"
) )
@ -30,6 +31,7 @@ func TestToken_SpendToken(t *testing.T) {
} }
func TestGenerateBlindedTokenBatch(t *testing.T) { func TestGenerateBlindedTokenBatch(t *testing.T) {
log.SetLevel(log.LevelDebug)
server := NewTokenServer() server := NewTokenServer()
clientTranscript := core.NewTranscript("privacyPass") clientTranscript := core.NewTranscript("privacyPass")

View File

@ -5,21 +5,24 @@ import (
"cwtch.im/tapir/primitives/core" "cwtch.im/tapir/primitives/core"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"github.com/bwesterb/go-ristretto" "github.com/bwesterb/go-ristretto"
"golang.org/x/crypto/sha3" "golang.org/x/crypto/sha3"
"sync"
) )
// TokenServer implements a token server. // TokenServer implements a token server.
type TokenServer struct { type TokenServer struct {
k *ristretto.Scalar k *ristretto.Scalar
Y *ristretto.Point Y *ristretto.Point
seen map[string]bool seen map[string]bool
mutex sync.Mutex
} }
// NewTokenServer generates a new TokenServer (used mostly for testing with ephemeral instances) // NewTokenServer generates a new TokenServer (used mostly for testing with ephemeral instances)
func NewTokenServer() TokenServer { func NewTokenServer() TokenServer {
k := new(ristretto.Scalar).Rand() k := new(ristretto.Scalar).Rand()
return TokenServer{k, new(ristretto.Point).ScalarMultBase(k), make(map[string]bool)} return TokenServer{k, new(ristretto.Point).ScalarMultBase(k), make(map[string]bool), sync.Mutex{}}
} }
// SignBlindedToken calculates kP for the given BlindedToken P // 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. // IsValid returns true a SpentToken is valid and has never been spent before, false otherwise.
func (ts *TokenServer) IsValid(token SpentToken, data []byte) bool { func (ts *TokenServer) 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 return false
} }
Ht := sha3.Sum256(token.t) Ht := sha3.Sum256(token.T)
log.Debugf("token: %x", Ht)
T := new(ristretto.Point).SetElligator(&Ht) T := new(ristretto.Point).SetElligator(&Ht)
W := new(ristretto.Point).ScalarMult(T, ts.k) W := new(ristretto.Point).ScalarMult(T, ts.k)
key := sha3.Sum256(append(token.t, W.Bytes()...)) key := sha3.Sum256(append(token.T, W.Bytes()...))
mac := hmac.New(sha3.New512, key[:]) mac := hmac.New(sha3.New512, key[:])
K := mac.Sum(data) K := mac.Sum(data)
log.Debugf("mac: \n%x\nK:%x\n", token.MAC, K)
result := hmac.Equal(token.MAC, K) result := hmac.Equal(token.MAC, K)
if result == true { if result == true {
ts.seen[hex.EncodeToString(token.t)] = true ts.seen[hex.EncodeToString(token.T)] = true
} }
return result return result
} }