tapir/primitives/auditablestore.go

107 lines
4.4 KiB
Go
Raw Normal View History

2019-09-14 23:44:19 +00:00
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
}