107 lines
4.4 KiB
Go
107 lines
4.4 KiB
Go
|
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
|
||
|
}
|