188 lines
6.7 KiB
Go
188 lines
6.7 KiB
Go
package auditable
|
|
|
|
// WARNING NOTE: This is a sketch implementation, Not suitable for production use. The real auditable store is still being designed.
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"errors"
|
|
"git.openprivacy.ca/cwtch.im/tapir/persistence"
|
|
"git.openprivacy.ca/cwtch.im/tapir/primitives"
|
|
"git.openprivacy.ca/cwtch.im/tapir/primitives/core"
|
|
"git.openprivacy.ca/openprivacy/log"
|
|
"golang.org/x/crypto/ed25519"
|
|
"sync"
|
|
)
|
|
|
|
// 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 {
|
|
SignedProof SignedProof
|
|
Messages []Message
|
|
}
|
|
|
|
//
|
|
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 Store struct {
|
|
state State
|
|
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 *Store) Init(identity primitives.Identity) {
|
|
as.identity = identity
|
|
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 *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.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 *Store) GetState() State {
|
|
as.mutex.Lock()
|
|
defer as.mutex.Unlock()
|
|
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 *Store) 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:]
|
|
}
|
|
|
|
// 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 {
|
|
as.state.Messages = append(as.state.Messages, m)
|
|
|
|
// We reconstruct the transcript
|
|
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, 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
|
|
// 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 *Store) VerifyFraudProof(fraudCommit []byte, signedFraudProof SignedProof, key ed25519.PublicKey) (bool, error) {
|
|
|
|
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(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.
|
|
// 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
|
|
}
|
|
|
|
// 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))
|
|
}
|