tapir/primitives/auditable/auditablestore.go

187 lines
6.7 KiB
Go
Raw Normal View History

2019-09-15 21:20:05 +00:00
package auditable
2019-09-14 23:44:19 +00:00
2019-11-26 21:10:09 +00:00
// WARNING NOTE: This is a sketch implementation, Not suitable for production use. The real auditable store is still being designed.
2019-09-14 23:44:19 +00:00
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"
2019-09-14 23:44:19 +00:00
"golang.org/x/crypto/ed25519"
2019-09-15 05:50:06 +00:00
"sync"
2019-09-14 23:44:19 +00:00
)
// SignedProof encapsulates a signed proof
2019-09-15 21:20:05 +00:00
type SignedProof []byte
2019-09-14 23:44:19 +00:00
// Message encapsulates a message for more readable code.
type Message []byte
// State defines an array of messages.
type State struct {
2019-09-15 21:20:05 +00:00
SignedProof SignedProof
Messages []Message
2019-09-14 23:44:19 +00:00
}
2019-09-15 21:20:05 +00:00
const (
auditableDataStoreProtocol = "auditable-data-store"
newMessage = "new-message"
commit = "commit"
collapse = "collapse"
)
// Store defines a cryptographically secure & auditable transcript of messages sent from multiple
2019-09-14 23:44:19 +00:00
// unrelated clients to a server.
2019-09-15 21:20:05 +00:00
type Store struct {
2019-09-14 23:44:19 +00:00
state State
2019-09-15 21:20:05 +00:00
identity primitives.Identity
2019-09-14 23:44:19 +00:00
transcript *core.Transcript
2019-09-15 05:50:06 +00:00
LatestCommit []byte
commits map[string]int
mutex sync.Mutex
2019-09-15 21:20:05 +00:00
db persistence.Service
2019-09-14 23:44:19 +00:00
}
// Init initializes an auditable store
2019-09-15 21:20:05 +00:00
func (as *Store) Init(identity primitives.Identity) {
2019-09-14 23:44:19 +00:00
as.identity = identity
2019-09-15 21:20:05 +00:00
as.transcript = core.NewTranscript(auditableDataStoreProtocol)
2019-09-15 05:50:06 +00:00
as.commits = make(map[string]int)
2019-09-14 23:44:19 +00:00
}
2019-09-15 21:20:05 +00:00
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
}
2019-09-14 23:44:19 +00:00
// Add adds a message to the auditable store
2019-09-15 21:20:05 +00:00
func (as *Store) add(message Message) SignedProof {
2019-09-15 05:50:06 +00:00
as.mutex.Lock()
defer as.mutex.Unlock()
2019-09-15 21:20:05 +00:00
as.transcript.AddToTranscript(newMessage, message)
as.LatestCommit = as.transcript.CommitToTranscript(commit)
2019-09-15 05:50:06 +00:00
as.state.Messages = append(as.state.Messages, message)
2019-09-15 21:20:05 +00:00
as.state.SignedProof = as.identity.Sign(as.LatestCommit)
as.commits[base64.StdEncoding.EncodeToString(as.LatestCommit)] = len(as.state.Messages) - 1
return as.state.SignedProof
2019-09-14 23:44:19 +00:00
}
// GetState returns the current auditable state
2019-09-15 21:20:05 +00:00
func (as *Store) GetState() State {
2019-09-15 05:50:06 +00:00
as.mutex.Lock()
defer as.mutex.Unlock()
2019-09-15 21:20:05 +00:00
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
2019-09-15 05:50:06 +00:00
}
// GetMessagesAfter provides access to messages after the given commit.
2019-09-15 21:20:05 +00:00
func (as *Store) GetMessagesAfter(latestCommit []byte) []Message {
2019-09-15 05:50:06 +00:00
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:]
2019-09-14 23:44:19 +00:00
}
2019-09-15 21:20:05 +00:00
// AppendState merges a given state onto our state, first verifying that the two transcripts align
func (as *Store) AppendState(state State) error {
2019-09-15 05:50:06 +00:00
next := len(as.state.Messages)
2019-09-15 21:20:05 +00:00
for i, m := range state.Messages {
2019-09-15 05:50:06 +00:00
as.state.Messages = append(as.state.Messages, m)
2019-09-14 23:44:19 +00:00
// We reconstruct the transcript
2019-09-15 21:20:05 +00:00
as.transcript.AddToTranscript(newMessage, m)
as.LatestCommit = as.transcript.CommitToTranscript(commit)
log.Debugf("Adding message %d commit: %x", next+i, as.LatestCommit)
2019-09-15 05:50:06 +00:00
as.commits[base64.StdEncoding.EncodeToString(as.LatestCommit)] = next + i
2019-09-14 23:44:19 +00:00
}
// 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.
2021-06-09 17:36:34 +00:00
if !ed25519.Verify(as.identity.PublicKey(), as.LatestCommit, state.SignedProof) {
2019-09-14 23:44:19 +00:00
return errors.New("state is not consistent, the server is malicious")
}
return nil
}
2019-09-15 21:20:05 +00:00
// 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})
}
2019-09-14 23:44:19 +00:00
// 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)
2019-09-15 21:20:05 +00:00
func (as *Store) VerifyFraudProof(fraudCommit []byte, signedFraudProof SignedProof, key ed25519.PublicKey) (bool, error) {
2019-09-14 23:44:19 +00:00
2021-06-09 17:36:34 +00:00
if !ed25519.Verify(key, fraudCommit, signedFraudProof) {
2019-09-14 23:44:19 +00:00
// 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")
}
2019-09-15 21:20:05 +00:00
_, exists := as.commits[base64.StdEncoding.EncodeToString(fraudCommit)]
2019-09-14 23:44:19 +00:00
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
}
2019-09-15 21:20:05 +00:00
// 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))
}