diff --git a/protocol/auditlog.go b/protocol/auditlog.go index a412c76..b7454d0 100644 --- a/protocol/auditlog.go +++ b/protocol/auditlog.go @@ -11,10 +11,21 @@ import ( ) type directoryHistory struct { + *AudState addr string - signKey sign.PublicKey snapshots map[uint64]*DirSTR - latestSTR *DirSTR +} + +// caller validates that initSTR is for epoch 0. +func newDirectoryHistory(addr string, signKey sign.PublicKey, initSTR *DirSTR) *directoryHistory { + a := NewAuditor(signKey, initSTR) + h := &directoryHistory{ + AudState: a, + addr: addr, + snapshots: make(map[uint64]*DirSTR), + } + h.updateVerifiedSTR(initSTR) + return h } // A ConiksAuditLog maintains the histories @@ -27,21 +38,25 @@ type directoryHistory struct { // chronological order. type ConiksAuditLog map[[crypto.HashSizeByte]byte]*directoryHistory -// updateLatestSTR inserts a new STR into a directory history; -// assumes the STR has been validated by the caller -func (h *directoryHistory) updateLatestSTR(newLatest *DirSTR) { - h.snapshots[newLatest.Epoch] = newLatest - h.latestSTR = newLatest +// updateVerifiedSTR inserts the latest verified STR into a directory history; +// assumes the STRs have been validated by the caller. +func (h *directoryHistory) updateVerifiedSTR(newVerified *DirSTR) { + h.Update(newVerified) + h.snapshots[newVerified.Epoch] = newVerified } -// caller validates that initSTR is for epoch 0 -func newDirectoryHistory(addr string, signKey sign.PublicKey, initSTR *DirSTR) *directoryHistory { - h := new(directoryHistory) - h.addr = addr - h.signKey = signKey - h.snapshots = make(map[uint64]*DirSTR) - h.updateLatestSTR(initSTR) - return h +// Audit checks that a directory's STR history +// is linear and updates the auditor's state +// if the checks pass. +// Audit() first checks the oldest STR in the +// STR range received in message against the h.verfiedSTR, +// and then verifies the remaining STRs in msg, and +// finally updates the snapshots if the checks pass. +// Audit() is called when an auditor receives new STRs +// from a directory. +func (h *directoryHistory) Audit(msg *Response) error { + // TODO: Implement as part of the auditor-server protocol + return CheckPassed } // NewAuditLog constructs a new ConiksAuditLog. It creates an empty @@ -72,16 +87,16 @@ func (l ConiksAuditLog) get(dirInitHash [crypto.HashSizeByte]byte) (*directoryHi // signing key signKey, and a list of snapshots snaps representing the // directory's STR history so far, in chronological order. // Insert() returns an ErrAuditLog if the auditor attempts to create -// a new history for a known directory, an ErrMalformedDirectoryMessage -// if oldSTRs is malformed, and nil otherwise. +// a new history for a known directory, and nil otherwise. // Insert() only creates the initial entry in the log for addr. Use Update() // to insert newly observed STRs for addr in subsequent epochs. +// Insert() assumes that the caller +// has called Audit() on snaps before calling Insert(). // FIXME: pass Response message as param // masomel: will probably want to write a more generic function // for "catching up" on a history in case an auditor misses epochs func (l ConiksAuditLog) Insert(addr string, signKey sign.PublicKey, snaps []*DirSTR) error { - // make sure we're getting an initial STR at the very least if len(snaps) < 1 || snaps[0].Epoch != 0 { return ErrMalformedDirectoryMessage @@ -100,6 +115,8 @@ func (l ConiksAuditLog) Insert(addr string, signKey sign.PublicKey, // create the new directory history h = newDirectoryHistory(addr, signKey, snaps[0]) + // FIXME: remove this check --> caller calls Audit() before + // this function // add each STR into the history // start at 1 since we've inserted the initial STR above // This loop automatically catches if snaps is malformed @@ -112,41 +129,43 @@ func (l ConiksAuditLog) Insert(addr string, signKey sign.PublicKey, // verify the consistency of each new STR before inserting // into the audit log - if err := verifySTRConsistency(signKey, h.latestSTR, str); err != nil { + if err := h.verifySTRConsistency(h.VerifiedSTR(), str); err != nil { return err } - h.updateLatestSTR(snaps[i]) + h.updateVerifiedSTR(snaps[i]) } - // Finally, add the new history to the log l.set(dirInitHash, h) return nil } -// Update verifies the consistency of a newly observed STR newSTR for -// the directory addr, and inserts the newSTR into addr's directory history -// if the checks (i.e. STR signature and hash chain verifications) pass. -// Update() returns nil if the checks pass, and the appropriate consistency -// check error otherwise. Update() assumes that Insert() has been called for -// addr prior to its first call and thereby expects that an entry for addr -// exists in the audit log l. +// Update inserts a newly observed STR newSTR into the log entry for the +// directory history given by dirInitHash (hash of direcotry's initial STR). +// Update() assumes that Insert() has been called for +// dirInitHash prior to its first call and thereby expects that an +// entry for addr exists in the audit log l, and that the caller +// has called Audit() on newSTR before calling Update(). +// Update() returns ErrAuditLog if the audit log doesn't contain an +// entry for dirInitHash. // FIXME: pass Response message as param func (l ConiksAuditLog) Update(dirInitHash [crypto.HashSizeByte]byte, newSTR *DirSTR) error { - // error if we want to update the entry for an addr we don't know h, ok := l.get(dirInitHash) if !ok { return ErrAuditLog } - if err := verifySTRConsistency(h.signKey, h.latestSTR, newSTR); err != nil { + // FIXME: remove this check --> caller calls Audit() before this + // function + if err := h.verifySTRConsistency(h.VerifiedSTR(), newSTR); err != nil { return err } // update the latest STR - h.updateLatestSTR(newSTR) + // FIXME: use STR slice from Response msg + h.updateVerifiedSTR(newSTR) return nil } @@ -170,7 +189,6 @@ func (l ConiksAuditLog) Update(dirInitHash [crypto.HashSizeByte]byte, newSTR *Di // message.NewErrorResponse(ReqUnknownDirectory) tuple. func (l ConiksAuditLog) GetObservedSTRs(req *AuditingRequest) (*Response, ErrorCode) { - // make sure we have a history for the requested directory in the log h, ok := l.get(req.DirInitSTRHash) if !ok { @@ -178,7 +196,7 @@ func (l ConiksAuditLog) GetObservedSTRs(req *AuditingRequest) (*Response, } // make sure the request is well-formed - if req.EndEpoch > h.latestSTR.Epoch || req.StartEpoch > req.EndEpoch { + if req.EndEpoch > h.VerifiedSTR().Epoch || req.StartEpoch > req.EndEpoch { return NewErrorResponse(ErrMalformedClientMessage), ErrMalformedClientMessage } diff --git a/protocol/auditor.go b/protocol/auditor.go new file mode 100644 index 0000000..4d10dde --- /dev/null +++ b/protocol/auditor.go @@ -0,0 +1,174 @@ +// This module implements a generic CONIKS auditor, i.e. the +// functionality that clients and auditors need to verify +// a server's STR history. + +package protocol + +import ( + "fmt" + "reflect" + + "github.com/coniks-sys/coniks-go/crypto" + "github.com/coniks-sys/coniks-go/crypto/sign" +) + +// Auditor provides a generic interface allowing different +// auditor types to implement specific auditing functionality. +type Auditor interface { + AuditDirectory([]*DirSTR) error +} + +// AudState verifies the hash chain of a specific directory. +type AudState struct { + signKey sign.PublicKey + verifiedSTR *DirSTR +} + +var _ Auditor = (*AudState)(nil) + +// NewAuditor instantiates a new auditor state from a persistance storage. +func NewAuditor(signKey sign.PublicKey, verified *DirSTR) *AudState { + a := &AudState{ + signKey: signKey, + verifiedSTR: verified, + } + return a +} + +// VerifiedSTR returns the newly verified STR. +func (a *AudState) VerifiedSTR() *DirSTR { + return a.verifiedSTR +} + +// Update updates the auditor's verifiedSTR to newSTR +func (a *AudState) Update(newSTR *DirSTR) { + a.verifiedSTR = newSTR +} + +// compareWithVerified checks whether the received STR is the same as +// the verified STR in the AudState using reflect.DeepEqual(). +func (a *AudState) compareWithVerified(str *DirSTR) error { + if reflect.DeepEqual(a.verifiedSTR, str) { + return nil + } + return CheckBadSTR +} + +// verifySTRConsistency checks the consistency between 2 snapshots. +// It uses the signing key signKey to verify the STR's signature. +// The signKey param either comes from a client's +// pinned signing key in its consistency state, +// or an auditor's pinned signing key in its history. +func (a *AudState) verifySTRConsistency(prevSTR, str *DirSTR) error { + // verify STR's signature + if !a.signKey.Verify(str.Serialize(), str.Signature) { + return CheckBadSignature + } + if str.VerifyHashChain(prevSTR) { + return nil + } + + // TODO: verify the directory's policies as well. See #115 + return CheckBadSTR +} + +// checkSTRAgainstVerified checks an STR str against the a.verifiedSTR. +// If str's Epoch is the same as the verified, checkSTRAgainstVerified() +// compares the two STRs directly. If str is one epoch ahead of the +// a.verifiedSTR, checkSTRAgainstVerified() checks the consistency between +// the two STRs. +// checkSTRAgainstVerified() returns nil if the check passes, +// or the appropriate consistency check error if any of the checks fail, +// or str's epoch is anything other than the same or one ahead of +// a.verifiedSTR. +func (a *AudState) checkSTRAgainstVerified(str *DirSTR) error { + // FIXME: check whether the STR was issued on time and whatnot. + // Maybe it has something to do w/ #81 and client + // transitioning between epochs. + // Try to verify w/ what's been saved + + // FIXME: we are returning the error immediately + // without saving the inconsistent STR + // see: https://github.com/coniks-sys/coniks-go/pull/74#commitcomment-19804686 + switch { + case str.Epoch == a.verifiedSTR.Epoch: + // Checking an STR in the same epoch + if err := a.compareWithVerified(str); err != nil { + return err + } + case str.Epoch == a.verifiedSTR.Epoch+1: + // Otherwise, expect that we've entered a new epoch + if err := a.verifySTRConsistency(a.verifiedSTR, str); err != nil { + return err + } + default: + return CheckBadSTR + } + + return nil +} + +// verifySTRRange checks the consistency of a range +// of a directory's STRs. It begins by verifying the STR consistency between +// the given prevSTR and the first STR in the given range, and +// then verifies the consistency between each subsequent STR pair. +func (a *AudState) verifySTRRange(prevSTR *DirSTR, strs []*DirSTR) error { + prev := prevSTR + for i := 0; i < len(strs); i++ { + str := strs[i] + if str == nil { + // FIXME: if this comes from the auditor, this + // should really be an ErrMalformedAuditorMessage + return ErrMalformedDirectoryMessage + } + + // verify the consistency of each STR in the range + if err := a.verifySTRConsistency(prev, str); err != nil { + return err + } + + prev = str + } + + return nil +} + +// AuditDirectory validates a range of STRs received from a CONIKS directory. +// AuditDirectory() checks the consistency of the oldest STR in the range +// against the verifiedSTR, and verifies the remaining +// range if the message contains more than one STR. +// AuditDirectory() returns the appropriate consistency check error +// if any of the checks fail, or nil if the checks pass. +func (a *AudState) AuditDirectory(strs []*DirSTR) error { + // validate strs + if len(strs) == 0 { + return ErrMalformedDirectoryMessage + } + + // check STR against the latest verified STR + if err := a.checkSTRAgainstVerified(strs[0]); err != nil { + return err + } + + // verify the entire range if we have received more than one STR + if len(strs) > 1 { + if err := a.verifySTRRange(strs[0], strs[1:]); err != nil { + return err + } + } + + return nil +} + +// ComputeDirectoryIdentity returns the hash of +// the directory's initial STR as a byte array. +// It panics if the STR isn't an initial STR (i.e. str.Epoch != 0). +func ComputeDirectoryIdentity(str *DirSTR) [crypto.HashSizeByte]byte { + if str.Epoch != 0 { + panic(fmt.Sprintf("[coniks] Expect epoch 0, got %x", str.Epoch)) + } + + var initSTRHash [crypto.HashSizeByte]byte + copy(initSTRHash[:], crypto.Digest(str.Signature)) + return initSTRHash +} diff --git a/protocol/common_test.go b/protocol/auditor_test.go similarity index 100% rename from protocol/common_test.go rename to protocol/auditor_test.go diff --git a/protocol/common.go b/protocol/common.go deleted file mode 100644 index 01f277b..0000000 --- a/protocol/common.go +++ /dev/null @@ -1,20 +0,0 @@ -package protocol - -import ( - "fmt" - - "github.com/coniks-sys/coniks-go/crypto" -) - -// ComputeDirectoryIdentity returns the hash of -// the directory's initial STR as a string. -// It panics if the STR isn't an initial STR (i.e. str.Epoch != 0). -func ComputeDirectoryIdentity(str *DirSTR) [crypto.HashSizeByte]byte { - if str.Epoch != 0 { - panic(fmt.Sprintf("[coniks] Expect epoch 0, got %x", str.Epoch)) - } - - var initSTRHash [crypto.HashSizeByte]byte - copy(initSTRHash[:], crypto.Digest(str.Signature)) - return initSTRHash -} diff --git a/protocol/consistencychecks.go b/protocol/consistencychecks.go index bc181a9..b0ee106 100644 --- a/protocol/consistencychecks.go +++ b/protocol/consistencychecks.go @@ -8,7 +8,6 @@ package protocol import ( "bytes" - "reflect" "github.com/coniks-sys/coniks-go/crypto/sign" m "github.com/coniks-sys/coniks-go/merkletree" @@ -26,15 +25,14 @@ import ( // subsequent responses from the ConiksDirectory to any // client request. type ConsistencyChecks struct { - // SavedSTR stores the latest verified signed tree root. - SavedSTR *DirSTR + // the auditor state stores the latest verified signed tree root + // as well as the server's signing key + *AudState Bindings map[string][]byte // extensions settings useTBs bool TBs map[string]*TemporaryBinding - - signKey sign.PublicKey } // NewCC creates an instance of ConsistencyChecks using @@ -45,11 +43,12 @@ func NewCC(savedSTR *DirSTR, useTBs bool, signKey sign.PublicKey) *ConsistencyCh if !useTBs { panic("[coniks] Currently the server is forced to use TBs") } + a := NewAuditor(signKey, savedSTR) cc := &ConsistencyChecks{ - SavedSTR: savedSTR, + AudState: a, Bindings: make(map[string][]byte), useTBs: useTBs, - signKey: signKey, + TBs: nil, } if useTBs { cc.TBs = make(map[string]*TemporaryBinding) @@ -57,6 +56,35 @@ func NewCC(savedSTR *DirSTR, useTBs bool, signKey sign.PublicKey) *ConsistencyCh return cc } +// CheckEquivocation checks for possible equivocation between +// an auditors' observed STRs and the client's own view. +// CheckEquivocation() first verifies the STR range received +// in msg if msg contains more than 1 STR, and +// then checks the most recent STR in msg against +// the cc.verifiedSTR. +// CheckEquivocation() is called when a client receives a response to a +// message.AuditingRequest from an auditor. +func (cc *ConsistencyChecks) CheckEquivocation(msg *Response) error { + if err := msg.validate(); err != nil { + return err.(ErrorCode) + } + + strs := msg.DirectoryResponse.(*STRHistoryRange) + + // verify the hashchain of the received STRs + // if we get more than 1 in our range + if len(strs.STR) > 1 { + if err := cc.verifySTRRange(strs.STR[0], strs.STR[1:]); err != nil { + return err + } + } + + // TODO: if the auditor has returned a more recent STR, + // should the client update its savedSTR? Should this + // force a new round of monitoring? + return cc.checkSTRAgainstVerified(strs.STR[len(strs.STR)-1]) +} + // HandleResponse verifies the directory's response for a request. // It first verifies the directory's returned status code of the request. // If the status code is not in the Errors array, it means @@ -102,16 +130,10 @@ func (cc *ConsistencyChecks) updateSTR(requestType int, msg *Response) error { switch requestType { case RegistrationType, KeyLookupType: str = msg.DirectoryResponse.(*DirectoryProof).STR[0] - // First response - if cc.SavedSTR == nil { - cc.SavedSTR = str - return nil - } - if err := cc.verifySTR(str); err == nil { - return nil - } - // Otherwise, expect that we've entered a new epoch - if err := verifySTRConsistency(cc.signKey, cc.SavedSTR, str); err != nil { + // The initial STR is pinned in the client + // so cc.verifiedSTR should never be nil + // FIXME: use STR slice from Response msg + if err := cc.AuditDirectory([]*DirSTR{str}); err != nil { return err } @@ -120,40 +142,9 @@ func (cc *ConsistencyChecks) updateSTR(requestType int, msg *Response) error { } // And update the saved STR - cc.SavedSTR = str - return nil -} - -// verifySTR checks whether the received STR is the same with -// the SavedSTR using reflect.DeepEqual(). -// FIXME: check whether the STR was issued on time and whatnot. -// Maybe it has something to do w/ #81 and client transitioning between epochs. -// Try to verify w/ what's been saved -// FIXME: make this generic so the auditor can also verify the timeliness of the -// STR etc. Might make sense to separate the comparison, which is only done on the client, -// from the rest. -func (cc *ConsistencyChecks) verifySTR(str *DirSTR) error { - if reflect.DeepEqual(cc.SavedSTR, str) { - return nil - } - return CheckBadSTR -} + cc.Update(str) -// verifySTRConsistency checks the consistency between 2 snapshots. -// It uses the signing key signKey to verify the STR's signature. -// The signKey param either comes from a client's -// pinned signing key, or an auditor's pinned signing key -// in its history. -func verifySTRConsistency(signKey sign.PublicKey, savedSTR, str *DirSTR) error { - // verify STR's signature - if !signKey.Verify(str.Serialize(), str.Signature) { - return CheckBadSignature - } - if str.VerifyHashChain(savedSTR) { - return nil - } - // TODO: verify the directory's policies as well. See #115 - return CheckBadSTR + return nil } func (cc *ConsistencyChecks) checkConsistency(requestType int, msg *Response,