diff --git a/client/coniksclient/internal/cmd/run.go b/client/coniksclient/internal/cmd/run.go index 23e816d..7a86270 100644 --- a/client/coniksclient/internal/cmd/run.go +++ b/client/coniksclient/internal/cmd/run.go @@ -46,7 +46,7 @@ func init() { func run(cmd *cobra.Command) { isDebugging, _ := strconv.ParseBool(cmd.Flag("debug").Value.String()) conf := loadConfigOrExit(cmd) - cc := p.NewCC(nil, true, conf.SigningPubKey) + cc := p.NewCC(nil, conf.SigningPubKey, nil, true, nil) state, err := terminal.MakeRaw(int(os.Stdin.Fd())) if err != nil { diff --git a/protocol/consistencychecks.go b/protocol/consistencychecks.go index 856b706..d504a33 100644 --- a/protocol/consistencychecks.go +++ b/protocol/consistencychecks.go @@ -27,7 +27,15 @@ import ( type ConsistencyChecks struct { // SavedSTR stores the latest verified signed tree root. SavedSTR *DirSTR + // Bindings stores all the verified name-to-key bindings. Bindings map[string][]byte + // RegEpoch keeps the registration epoch of each user. + // Entries are kept even the binding was inserted into the directory. + RegEpoch map[string]uint64 + + // oldSTR stores the previous verified STR. + // This is used as a cache to verify the TB. + oldSTR *DirSTR // extensions settings useTBs bool @@ -39,7 +47,8 @@ type ConsistencyChecks struct { // NewCC creates an instance of ConsistencyChecks using // a CONIKS directory's pinned STR at epoch 0, or // the consistency state read from persistent storage. -func NewCC(savedSTR *DirSTR, useTBs bool, signKey sign.PublicKey) *ConsistencyChecks { +func NewCC(savedSTR *DirSTR, signKey sign.PublicKey, regs map[string]uint64, + useTBs bool, tbs map[string]*TemporaryBinding) *ConsistencyChecks { // TODO: see #110 if !useTBs { panic("[coniks] Currently the server is forced to use TBs") @@ -49,9 +58,18 @@ func NewCC(savedSTR *DirSTR, useTBs bool, signKey sign.PublicKey) *ConsistencyCh Bindings: make(map[string][]byte), useTBs: useTBs, signKey: signKey, + RegEpoch: regs, + oldSTR: savedSTR, + } + if len(regs) == 0 { + cc.RegEpoch = make(map[string]uint64) } if useTBs { - cc.TBs = make(map[string]*TemporaryBinding) + if tbs == nil { + cc.TBs = make(map[string]*TemporaryBinding) + } else { + cc.TBs = tbs + } } return cc } @@ -79,8 +97,8 @@ func (cc *ConsistencyChecks) HandleResponse(requestType int, msg *Response, if _, ok := msg.DirectoryResponse.(*DirectoryProof); !ok { return ErrMalformedDirectoryMessage } - case MonitoringType, KeyLookupInEpochType: - if _, ok := msg.DirectoryResponse.(*DirectoryProofs); !ok { + case MonitoringType: + if _, ok := msg.DirectoryResponse.(*DirectoryProofs); !ok || msg.Error != ReqSuccess { return ErrMalformedDirectoryMessage } default: @@ -118,9 +136,34 @@ func (cc *ConsistencyChecks) updateSTR(requestType int, msg *Response) error { } // Otherwise, expect that we've entered a new epoch if err := cc.verifySTRConsistency(cc.SavedSTR, str); err != nil { + // FIXME: this check should be removed (see #173) return err } + case MonitoringType: + // FIXME: we are returning the error immediately + // without saving the inconsistent STR + // see: https://github.com/coniks-sys/coniks-go/pull/74#commitcomment-19804686 + strs := msg.DirectoryResponse.(*DirectoryProofs).STR + switch { + case strs[0].Epoch == cc.SavedSTR.Epoch: + if err := cc.verifySTR(strs[0]); err != nil { + return err + } + case strs[0].Epoch == cc.SavedSTR.Epoch+1: + if err := cc.verifySTRConsistency(cc.SavedSTR, strs[0]); err != nil { + return err + } + default: + return CheckUnexpectedMonitoringEpoch + } + + for i := 1; i < len(strs); i++ { + if err := cc.verifySTRConsistency(strs[i-1], strs[i]); err != nil { + return err + } + } + str = strs[len(strs)-1] default: panic("[coniks] Unknown request type") } @@ -132,6 +175,9 @@ func (cc *ConsistencyChecks) updateSTR(requestType int, msg *Response) error { // 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 func (cc *ConsistencyChecks) verifySTR(str *DirSTR) error { if reflect.DeepEqual(cc.SavedSTR, str) { return nil @@ -163,6 +209,8 @@ func (cc *ConsistencyChecks) checkConsistency(requestType int, msg *Response, err = cc.verifyRegistration(msg, uname, key) case KeyLookupType: err = cc.verifyKeyLookup(msg, uname, key) + case MonitoringType: + err = cc.verifyMonitoring(msg, uname, key) default: panic("[coniks] Unknown request type") } @@ -214,6 +262,28 @@ func (cc *ConsistencyChecks) verifyKeyLookup(msg *Response, return CheckPassed } +func (cc *ConsistencyChecks) verifyMonitoring(msg *Response, + uname string, key []byte) error { + if _, ok := cc.RegEpoch[uname]; !ok { + // TODO: panic for now since we haven't supported #106 yet + panic("[coniks] Cannot perform monitoring for unregistered user name") + } + + dfs := msg.DirectoryResponse.(*DirectoryProofs) + for i := 0; i < len(dfs.STR); i++ { + str := dfs.STR[i] + ap := dfs.AP[i] + if ap.ProofType() != m.ProofOfInclusion { + return CheckBadAuthPath + } + if err := verifyAuthPath(uname, key, ap, str); err != nil { + return err + } + } + + return CheckPassed +} + func verifyAuthPath(uname string, key []byte, ap *m.AuthenticationPath, str *DirSTR) error { // verify VRF Index vrfKey := str.Policies.VrfPublicKey @@ -256,20 +326,22 @@ func (cc *ConsistencyChecks) updateTBs(requestType int, msg *Response, return err } cc.TBs[uname] = df.TB + cc.RegEpoch[uname] = cc.SavedSTR.Epoch } return nil case KeyLookupType: df := msg.DirectoryResponse.(*DirectoryProof) ap := df.AP - str := df.STR proofType := ap.ProofType() switch { - case msg.Error == ReqSuccess && proofType == m.ProofOfInclusion: - if err := cc.verifyFulfilledPromise(uname, str, ap); err != nil { - return err - } - delete(cc.TBs, uname) + // FIXME: We don' support this for now, + // until we know how to cache the issued epoch. (See #116) + // case msg.Error == ReqSuccess && proofType == m.ProofOfInclusion: + // if err := cc.verifyFulfilledPromise(uname, str, ap); err != nil { + // return err + // } + // delete(cc.TBs, uname) case msg.Error == ReqSuccess && proofType == m.ProofOfAbsence: if err := cc.verifyReturnedPromise(df, key); err != nil { @@ -278,6 +350,13 @@ func (cc *ConsistencyChecks) updateTBs(requestType int, msg *Response, cc.TBs[uname] = df.TB } + case MonitoringType: + dfs := msg.DirectoryResponse.(*DirectoryProofs) + if err := cc.verifyFulfilledPromise(uname, dfs.STR[0], dfs.AP[0]); err != nil { + return err + } + delete(cc.TBs, uname) + default: panic("[coniks] Unknown request type") } @@ -288,9 +367,9 @@ func (cc *ConsistencyChecks) updateTBs(requestType int, msg *Response, // in the directory as promised. func (cc *ConsistencyChecks) verifyFulfilledPromise(uname string, str *DirSTR, ap *m.AuthenticationPath) error { - // FIXME: Which epoch did this lookup happen in? if tb, ok := cc.TBs[uname]; ok { - if !bytes.Equal(ap.LookupIndex, tb.Index) || + if cc.oldSTR.Epoch+1 != str.Epoch || + !bytes.Equal(ap.LookupIndex, tb.Index) || !bytes.Equal(ap.Leaf.Value, tb.Value) { return CheckBrokenPromise } diff --git a/protocol/consistencychecks_test.go b/protocol/consistencychecks_test.go index 81ac271..a77a036 100644 --- a/protocol/consistencychecks_test.go +++ b/protocol/consistencychecks_test.go @@ -3,6 +3,9 @@ package protocol import ( "bytes" "testing" + + "github.com/coniks-sys/coniks-go/crypto/sign" + "github.com/coniks-sys/coniks-go/merkletree" ) var ( @@ -30,6 +33,21 @@ func lookupAndVerify(d *ConiksDirectory, cc *ConsistencyChecks, return err, cc.HandleResponse(KeyLookupType, res, name, key) } +func monitorAndVerify(d *ConiksDirectory, cc *ConsistencyChecks, + name string, key []byte, startEp, endEp uint64) (error, error) { + request := &MonitoringRequest{ + Username: name, + StartEpoch: startEp, + EndEpoch: endEp, + } + res, err := d.Monitor(request) + return err, cc.HandleResponse(MonitoringType, res, name, key) +} + +func newTestVerifier(str *DirSTR, pk sign.PublicKey) *ConsistencyChecks { + return NewCC(str, pk, nil, true, nil) +} + func TestVerifyWithError(t *testing.T) { d, pk := NewTestDirectory(t, true) str := d.LatestSTR() @@ -40,7 +58,7 @@ func TestVerifyWithError(t *testing.T) { str2.Signature[0]++ str.SignedTreeRoot = &str2 - cc := NewCC(str, true, pk) + cc := newTestVerifier(str, pk) e1, e2 := registerAndVerify(d, cc, alice, key) if e1 != ReqSuccess { @@ -53,7 +71,7 @@ func TestVerifyWithError(t *testing.T) { func TestMalformedClientMessage(t *testing.T) { d, pk := NewTestDirectory(t, true) - cc := NewCC(d.LatestSTR(), true, pk) + cc := newTestVerifier(d.LatestSTR(), pk) request := &RegistrationRequest{ Username: "", // invalid username @@ -67,7 +85,7 @@ func TestMalformedClientMessage(t *testing.T) { func TestMalformedDirectoryMessage(t *testing.T) { d, pk := NewTestDirectory(t, true) - cc := NewCC(d.LatestSTR(), true, pk) + cc := newTestVerifier(d.LatestSTR(), pk) request := &RegistrationRequest{ Username: "alice", @@ -83,8 +101,7 @@ func TestMalformedDirectoryMessage(t *testing.T) { func TestVerifyRegistrationResponseWithTB(t *testing.T) { d, pk := NewTestDirectory(t, true) - - cc := NewCC(d.LatestSTR(), true, pk) + cc := newTestVerifier(d.LatestSTR(), pk) if e1, e2 := registerAndVerify(d, cc, alice, key); e1 != ReqSuccess || e2 != CheckPassed { t.Error(e1) @@ -126,41 +143,28 @@ func TestVerifyRegistrationResponseWithTB(t *testing.T) { } func TestVerifyFullfilledPromise(t *testing.T) { + N := 3 d, pk := NewTestDirectory(t, true) - - cc := NewCC(d.LatestSTR(), true, pk) + cc := newTestVerifier(d.LatestSTR(), pk) if e1, e2 := registerAndVerify(d, cc, alice, key); e1 != ReqSuccess || e2 != CheckPassed { t.Error(e1) t.Error(e2) } - if e1, e2 := registerAndVerify(d, cc, bob, key); e1 != ReqSuccess || e2 != CheckPassed { - t.Error(e1) - t.Error(e2) - } - if len(cc.TBs) != 2 { + if len(cc.TBs) != 1 { t.Fatal("Expect the directory to return signed promises") } - d.Update() - - for i := 0; i < 2; i++ { - if e1, e2 := lookupAndVerify(d, cc, alice, key); e1 != ReqSuccess || e2 != CheckPassed { - t.Error(e1) - t.Error(e2) - } - } - - // should contain the TBs of bob - if len(cc.TBs) != 1 || cc.TBs[bob] == nil { - t.Error("Expect the directory to insert the binding as promised") + for i := 0; i < N; i++ { + d.Update() } - if e1, e2 := lookupAndVerify(d, cc, bob, key); e1 != ReqSuccess || e2 != CheckPassed { + if e1, e2 := monitorAndVerify(d, cc, alice, key, cc.SavedSTR.Epoch+1, d.LatestSTR().Epoch); e1 != ReqSuccess || e2 != CheckPassed { t.Error(e1) t.Error(e2) } + if len(cc.TBs) != 0 { t.Error("Expect the directory to insert the binding as promised") } @@ -168,8 +172,7 @@ func TestVerifyFullfilledPromise(t *testing.T) { func TestVerifyKeyLookupResponseWithTB(t *testing.T) { d, pk := NewTestDirectory(t, true) - - cc := NewCC(d.LatestSTR(), true, pk) + cc := newTestVerifier(d.LatestSTR(), pk) // do lookup first if e1, e2 := lookupAndVerify(d, cc, alice, key); e1 != ReqNameNotFound || e2 != CheckPassed { @@ -225,3 +228,100 @@ func TestVerifyKeyLookupResponseWithTB(t *testing.T) { t.Error(e2) } } + +func TestVerifyMonitoring(t *testing.T) { + N := 5 + d, pk := NewTestDirectory(t, true) + cc := newTestVerifier(d.LatestSTR(), pk) + + registerAndVerify(d, cc, alice, key) + + // monitor binding was inserted + d.Update() + + if e1, e2 := monitorAndVerify(d, cc, alice, key, cc.SavedSTR.Epoch+1, d.LatestSTR().Epoch); e1 != ReqSuccess || e2 != CheckPassed { + t.Error(e1) + t.Error(e2) + } + for i := 0; i < N; i++ { + d.Update() + } + if e1, e2 := monitorAndVerify(d, cc, alice, key, cc.SavedSTR.Epoch+1, d.LatestSTR().Epoch); e1 != ReqSuccess || e2 != CheckPassed { + t.Error(e1) + t.Error(e2) + } +} + +// Expect the ConsistencyChecks to return CheckUnexpectedMonitoringEpoch: +// - If: StartEpoch > SavedEpoch + 1 +func TestVerifyMonitoringBadEpoch(t *testing.T) { + N := 5 + d, pk := NewTestDirectory(t, true) + cc := newTestVerifier(d.LatestSTR(), pk) + + registerAndVerify(d, cc, alice, key) + + for i := 0; i < N; i++ { + d.Update() + } + + if e1, e2 := monitorAndVerify(d, cc, alice, nil, cc.SavedSTR.Epoch+2, d.LatestSTR().Epoch); e1 != ReqSuccess || e2 != CheckUnexpectedMonitoringEpoch { + t.Error(e1) + t.Error(e2) + } +} + +func TestMalformedMonitoringResponse(t *testing.T) { + d, pk := NewTestDirectory(t, true) + cc := newTestVerifier(d.LatestSTR(), pk) + + // len(AP) == 0 + malformedResponse := &Response{ + Error: ReqSuccess, + DirectoryResponse: &DirectoryProofs{ + AP: nil, + STR: append([]*DirSTR{}, &DirSTR{}), + }, + } + if err := cc.HandleResponse(MonitoringType, malformedResponse, alice, key); err != ErrMalformedDirectoryMessage { + t.Error(err) + } + + // len(AP) != len(STR) + str2 := append([]*DirSTR{}, &DirSTR{}) + str2 = append(str2, &DirSTR{}) + malformedResponse = &Response{ + Error: ReqSuccess, + DirectoryResponse: &DirectoryProofs{ + AP: append([]*merkletree.AuthenticationPath{}, &merkletree.AuthenticationPath{}), + STR: str2, + }, + } + if err := cc.HandleResponse(MonitoringType, malformedResponse, alice, key); err != ErrMalformedDirectoryMessage { + t.Error(err) + } + + // len(STR) == 0 + malformedResponse = &Response{ + Error: ReqSuccess, + DirectoryResponse: &DirectoryProofs{ + AP: append([]*merkletree.AuthenticationPath{}, &merkletree.AuthenticationPath{}), + STR: nil, + }, + } + if err := cc.HandleResponse(MonitoringType, malformedResponse, alice, key); err != ErrMalformedDirectoryMessage { + t.Error(err) + } + + // Error != ReqSuccess + malformedResponse = &Response{ + Error: ReqNameNotFound, + DirectoryResponse: &DirectoryProofs{ + AP: append([]*merkletree.AuthenticationPath{}, &merkletree.AuthenticationPath{}), + STR: nil, + }, + } + if err := cc.HandleResponse(MonitoringType, malformedResponse, alice, key); err != ErrMalformedDirectoryMessage { + t.Error(err) + } +} diff --git a/protocol/error.go b/protocol/error.go index 866d508..efff2fc 100644 --- a/protocol/error.go +++ b/protocol/error.go @@ -36,6 +36,7 @@ const ( CheckBadSTR CheckBadPromise CheckBrokenPromise + CheckUnexpectedMonitoringEpoch ) // Errors contains codes indicating the client @@ -59,16 +60,17 @@ var ( ErrDirectory: "[coniks] Directory error", ErrMalformedDirectoryMessage: "[coniks] Malformed directory message", - CheckPassed: "[coniks] Consistency checks passed", - CheckBadSignature: "[coniks] Directory's signature on STR or TB is invalid", - CheckBadVRFProof: "[coniks] Returned index is not valid for the given name", - CheckBindingsDiffer: "[coniks] The key in the binding is inconsistent with our expectation", - CheckBadCommitment: "[coniks] The name-to-key binding commitment is not verifiable", - CheckBadLookupIndex: "[coniks] The lookup index is inconsistent with the index of the proof node", - CheckBadAuthPath: "[coniks] Returned binding is inconsistent with the tree root hash", - CheckBadSTR: "[coniks] The hash chain is inconsistent", - CheckBadPromise: "[coniks] The directory returned an invalid registration promise", - CheckBrokenPromise: "[coniks] The directory broke the registration promise", + CheckPassed: "[coniks] Consistency checks passed", + CheckBadSignature: "[coniks] Directory's signature on STR or TB is invalid", + CheckBadVRFProof: "[coniks] Returned index is not valid for the given name", + CheckBindingsDiffer: "[coniks] The key in the binding is inconsistent with our expectation", + CheckBadCommitment: "[coniks] The name-to-key binding commitment is not verifiable", + CheckBadLookupIndex: "[coniks] The lookup index is inconsistent with the index of the proof node", + CheckBadAuthPath: "[coniks] Returned binding is inconsistent with the tree root hash", + CheckBadSTR: "[coniks] The hash chain is inconsistent", + CheckBadPromise: "[coniks] The directory returned an invalid registration promise", + CheckBrokenPromise: "[coniks] The directory broke the registration promise", + CheckUnexpectedMonitoringEpoch: "[coniks] The monitoring epoch is invalid", } ) diff --git a/protocol/message.go b/protocol/message.go index c666568..df68995 100644 --- a/protocol/message.go +++ b/protocol/message.go @@ -227,7 +227,9 @@ func (msg *Response) validate() error { } return nil case *DirectoryProofs: - // TODO: also do above assertions here + if len(df.AP) == 0 || len(df.AP) != len(df.STR) { + return ErrMalformedDirectoryMessage + } return nil default: panic("[coniks] Malformed response")