diff --git a/client/coniksclient/internal/cmd/run.go b/client/coniksclient/internal/cmd/run.go index 23e816d..bc923d3 100644 --- a/client/coniksclient/internal/cmd/run.go +++ b/client/coniksclient/internal/cmd/run.go @@ -123,6 +123,11 @@ func register(cc *p.ConsistencyChecks, conf *client.Config, name string, key str } u, _ := url.Parse(regAddress) switch u.Scheme { + case "https": + res, err = testutil.NewHTTPSClient(req, regAddress) + if err != nil { + return ("Error while receiving response: " + err.Error()) + } case "tcp": res, err = testutil.NewTCPClient(req, regAddress) if err != nil { @@ -176,6 +181,11 @@ func keyLookup(cc *p.ConsistencyChecks, conf *client.Config, name string) string var res []byte u, _ := url.Parse(conf.Address) switch u.Scheme { + case "https": + res, err = testutil.NewHTTPSClient(req, conf.Address) + if err != nil { + return ("Error while receiving response: " + err.Error()) + } case "tcp": res, err = testutil.NewTCPClient(req, conf.Address) if err != nil { diff --git a/keyserver/README.md b/keyserver/README.md index 63b453d..c7a31f1 100644 --- a/keyserver/README.md +++ b/keyserver/README.md @@ -45,7 +45,7 @@ for "read-only" requests (lookups, monitoring etc). [policies] ... [[addresses]] - address = "tcp://public.server.address:port" + address = "tcp://public.server.address:port" # or "https://public.server.address:port" allow_registration = true cert = "server.pem" key = "server.key" diff --git a/keyserver/listener.go b/keyserver/listener.go index 6e19784..99d2d6c 100644 --- a/keyserver/listener.go +++ b/keyserver/listener.go @@ -4,12 +4,22 @@ import ( "bytes" "crypto/tls" "io" + "io/ioutil" "net" + "net/http" "time" . "github.com/coniks-sys/coniks-go/protocol" ) +func (server *ConiksServer) makeHTTPSHandler(acceptableTypes map[int]bool) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + body, _ := ioutil.ReadAll(r.Body) + res, _ := server.makeHandler(acceptableTypes)(body) + w.Write(res) + } +} + func (server *ConiksServer) handleRequests(ln net.Listener, tlsConfig *tls.Config, handler func(msg []byte) ([]byte, error)) { defer ln.Close() diff --git a/keyserver/server.go b/keyserver/server.go index 86b62ce..247d1fd 100644 --- a/keyserver/server.go +++ b/keyserver/server.go @@ -1,10 +1,12 @@ package keyserver import ( + "context" "crypto/tls" "fmt" "io/ioutil" "net" + "net/http" "net/url" "os" "os/signal" @@ -171,22 +173,49 @@ func (server *ConiksServer) Run(addrs []*Address) { server.epochUpdate() server.waitStop.Done() }() - hasRegistrationPerm := false for i := 0; i < len(addrs); i++ { addr := addrs[i] + perms := updatePerms(addr) hasRegistrationPerm = hasRegistrationPerm || addr.AllowRegistration - ln, tlsConfig, perms := resolveAndListen(addr) - server.waitStop.Add(1) - go func() { - verb := "Listening" - if addr.AllowRegistration { - verb = "Accepting registrations" + u, err := url.Parse(addr.Address) + if err != nil { + panic(err) + } + switch u.Scheme { + case "https": + mux := http.NewServeMux() + mux.HandleFunc("/", server.makeHTTPSHandler(perms)) + ln, tlsConfig := resolveAndListen(addr) + httpSrv := &http.Server{ + Addr: u.Host, + Handler: mux, + TLSConfig: tlsConfig, } - server.logger.Info(verb, "address", addr.Address) - server.handleRequests(ln, tlsConfig, server.makeHandler(perms)) - server.waitStop.Done() - }() + go func() { + httpSrv.Serve(ln) + }() + server.waitStop.Add(1) + go func() { + <-server.stop + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + httpSrv.Shutdown(ctx) + server.waitStop.Done() + }() + case "tcp", "unix": + ln, tlsConfig := resolveAndListen(addr) + server.waitStop.Add(1) + go func() { + server.handleRequests(ln, tlsConfig, server.makeHandler(perms)) + server.waitStop.Done() + }() + } + verb := "Listening" + if addr.AllowRegistration { + verb = "Accepting registrations" + } + server.logger.Info(verb, "address", addr.Address) } if !hasRegistrationPerm { @@ -236,20 +265,23 @@ func (server *ConiksServer) updatePolicies() { } } -func resolveAndListen(addr *Address) (ln net.Listener, - tlsConfig *tls.Config, - perms map[int]bool) { - perms = make(map[int]bool) - perms[protocol.KeyLookupType] = true - perms[protocol.KeyLookupInEpochType] = true - perms[protocol.MonitoringType] = true - perms[protocol.RegistrationType] = addr.AllowRegistration - +func resolveAndListen(addr *Address) (ln net.Listener, tlsConfig *tls.Config) { u, err := url.Parse(addr.Address) if err != nil { panic(err) } switch u.Scheme { + case "https": + cer, err := tls.LoadX509KeyPair(addr.TLSCertPath, addr.TLSKeyPath) + if err != nil { + panic(err) + } + tlsConfig = &tls.Config{Certificates: []tls.Certificate{cer}} + ln, err = tls.Listen("tcp", u.Host, tlsConfig) + if err != nil { + panic(err) + } + return case "tcp": // force to use TLS cer, err := tls.LoadX509KeyPair(addr.TLSCertPath, addr.TLSKeyPath) @@ -281,6 +313,15 @@ func resolveAndListen(addr *Address) (ln net.Listener, } } +func updatePerms(addr *Address) map[int]bool { + perms := make(map[int]bool) + perms[protocol.KeyLookupType] = true + perms[protocol.KeyLookupInEpochType] = true + perms[protocol.MonitoringType] = true + perms[protocol.RegistrationType] = addr.AllowRegistration + return perms +} + // Shutdown closes all of the server's connections and shuts down the server. func (server *ConiksServer) Shutdown() error { close(server.stop) diff --git a/keyserver/server_test.go b/keyserver/server_test.go index 9d202f4..713d189 100644 --- a/keyserver/server_test.go +++ b/keyserver/server_test.go @@ -26,7 +26,6 @@ var registrationMsg = ` } } ` - var keylookupMsg = ` { "type": 1, @@ -48,7 +47,6 @@ func startServer(t *testing.T, epDeadline Timestamp, useBot bool, policiesPath s if err != nil { t.Fatal(err) } - addrs := []*Address{ &Address{ Address: testutil.PublicConnection, @@ -57,6 +55,12 @@ func startServer(t *testing.T, epDeadline Timestamp, useBot bool, policiesPath s AllowRegistration: !useBot, }, } + addrs = append(addrs, &Address{ + Address: testutil.PublicHTTPSConnection, + TLSCertPath: path.Join(dir, "server.pem"), + TLSKeyPath: path.Join(dir, "server.key"), + AllowRegistration: !useBot, + }) if useBot { addrs = append(addrs, &Address{ Address: testutil.LocalConnection, @@ -77,7 +81,6 @@ func startServer(t *testing.T, epDeadline Timestamp, useBot bool, policiesPath s Path: path.Join(dir, "coniksserver.log"), }, } - server := NewConiksServer(conf) server.Run(conf.Addresses) return server, func() { @@ -101,7 +104,8 @@ func TestResolveAddresses(t *testing.T) { TLSCertPath: path.Join(dir, "server.pem"), TLSKeyPath: path.Join(dir, "server.key"), } - ln, _, perms := resolveAndListen(addr) + perms := updatePerms(addr) + ln, _ := resolveAndListen(addr) defer ln.Close() if perms[RegistrationType] != false { t.Error("Expect disallowing registration permission.") @@ -112,7 +116,8 @@ func TestResolveAddresses(t *testing.T) { Address: testutil.LocalConnection, AllowRegistration: true, } - ln, _, perms = resolveAndListen(addr) + perms = updatePerms(addr) + ln, _ = resolveAndListen(addr) defer ln.Close() if perms[RegistrationType] != true { t.Error("Expect allowing registration permission.") @@ -142,7 +147,7 @@ func TestServerReloadPoliciesWithError(t *testing.T) { <-timer.C } -func TestAcceptOutsideRegistrationRequests(t *testing.T) { +func TestAcceptOutsideRegistrationTCPRequests(t *testing.T) { _, teardown := startServer(t, 60, false, "") defer teardown() rev, err := testutil.NewTCPClientDefault([]byte(registrationMsg)) @@ -160,6 +165,24 @@ func TestAcceptOutsideRegistrationRequests(t *testing.T) { } } +func TestAcceptOutsideRegistrationHTTPSRequests(t *testing.T) { + _, teardown := startServer(t, 60, false, "") + defer teardown() + rev, err := testutil.NewHTTPSClientDefault([]byte(registrationMsg)) + if err != nil { + t.Error(err) + } + var response testutil.ExpectingDirProofResponse + err = json.Unmarshal(rev, &response) + if err != nil { + t.Log(string(rev)) + t.Error(err) + } + if response.Error != ReqSuccess { + t.Error("Expect a successful registration", "got", response.Error) + } +} + func TestBotSendsRegistration(t *testing.T) { _, teardown := startServer(t, 60, true, "") defer teardown() @@ -180,7 +203,7 @@ func TestBotSendsRegistration(t *testing.T) { } } -func TestSendsRegistrationFromOutside(t *testing.T) { +func TestSendsTCPRegistrationFromOutside(t *testing.T) { _, teardown := startServer(t, 60, true, "") defer teardown() @@ -198,6 +221,24 @@ func TestSendsRegistrationFromOutside(t *testing.T) { } } +func TestSendsHTTPSRegistrationFromOutside(t *testing.T) { + _, teardown := startServer(t, 60, true, "") + defer teardown() + + rev, err := testutil.NewHTTPSClientDefault([]byte(registrationMsg)) + if err != nil { + t.Fatal(err) + } + var response Response + err = json.Unmarshal(rev, &response) + if err != nil { + t.Fatal(err) + } + if response.Error != ErrMalformedClientMessage { + t.Fatalf("Expect error code %d", ErrMalformedClientMessage) + } +} + func TestUpdateDirectory(t *testing.T) { server, teardown := startServer(t, 1, true, "") defer teardown() @@ -349,7 +390,7 @@ func TestRegisterAndLookupInTheSameEpoch(t *testing.T) { } } -func TestRegisterAndLookup(t *testing.T) { +func TestRegisterAndTCPLookup(t *testing.T) { server, teardown := startServer(t, 1, true, "") defer teardown() @@ -392,6 +433,49 @@ func TestRegisterAndLookup(t *testing.T) { } } +func TestRegisterAndHTTPSLookup(t *testing.T) { + server, teardown := startServer(t, 1, true, "") + defer teardown() + + _, err := testutil.NewUnixClientDefault([]byte(registrationMsg)) + if err != nil { + t.Fatal(err) + } + + server.dir.Update() + rev, err := testutil.NewHTTPSClientDefault([]byte(keylookupMsg)) + if err != nil { + t.Fatal(err) + } + + var res testutil.ExpectingDirProofResponse + err = json.Unmarshal(rev, &res) + if err != nil { + t.Fatal(err) + } + if res.Error != ReqSuccess { + t.Fatal("Expect no error", "got", res.Error) + } + if res.DirectoryResponse.STR == nil { + t.Fatal("Expect the latets STR") + } + + var str testutil.ExpectingSTR + err = json.Unmarshal(res.DirectoryResponse.STR, &str) + if err != nil { + t.Fatal(err) + } + if str.Epoch == 0 { + t.Fatal("Expect STR with epoch > 0") + } + if res.DirectoryResponse.AP == nil { + t.Fatal("Expect a proof of inclusion") + } + if res.DirectoryResponse.TB != nil { + t.Fatal("Expect returned TB is nil") + } +} + func TestKeyLookup(t *testing.T) { server, teardown := startServer(t, 60, true, "") defer teardown() diff --git a/keyserver/testutil/testutil.go b/keyserver/testutil/testutil.go index 5958141..574285d 100644 --- a/keyserver/testutil/testutil.go +++ b/keyserver/testutil/testutil.go @@ -23,6 +23,7 @@ import ( "io/ioutil" "math/big" "net" + "net/http" "net/url" "os" "path" @@ -37,6 +38,8 @@ const ( TestDir = "coniksServerTest" // PublicConnection is the default address for TCP connections PublicConnection = "tcp://127.0.0.1:3000" + // PublicHTTPSConnection is the default address for HTTPS connections + PublicHTTPSConnection = "https://127.0.0.1:4430" // LocalConnection is the default address for Unix socket connections LocalConnection = "unix:///tmp/conikstest.sock" ) @@ -151,7 +154,6 @@ func NewTCPClient(msg []byte, address string) ([]byte, error) { defer conn.Close() tlsConn := tls.Client(conn, conf) - _, err = tlsConn.Write([]byte(msg)) if err != nil { return nil, err @@ -178,6 +180,31 @@ func NewTCPClientDefault(msg []byte) ([]byte, error) { return NewTCPClient(msg, PublicConnection) } +// NewHTTPSClient creates a basic test client that sends a given +// request msg to the server listening at the given address +// via an HTTPS connection. +func NewHTTPSClient(msg []byte, address string) ([]byte, error) { + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + client := &http.Client{Transport: tr} + resp, err := client.Post(address, "application/json", bytes.NewBuffer(msg)) + if err != nil { + panic(err) + } + defer resp.Body.Close() + + body, _ := ioutil.ReadAll(resp.Body) + return body, nil +} + +// NewHTTPSClientDefault creates a basic test client that sends a given +// request msg to a server listening at the default PublicConnection +// address. +func NewHTTPSClientDefault(msg []byte) ([]byte, error) { + return NewHTTPSClient(msg, PublicHTTPSConnection) +} + // NewUnixClient creates a basic test client that sends a given // request msg to the server listening at the given address // via a Unix socket connection. diff --git a/protocol/auditlog.go b/protocol/auditlog.go new file mode 100644 index 0000000..a412c76 --- /dev/null +++ b/protocol/auditlog.go @@ -0,0 +1,193 @@ +// This module implements a CONIKS audit log that a CONIKS auditor +// maintains. +// An audit log is a mirror of many CONIKS key directories' STR history, +// allowing CONIKS clients to audit the CONIKS directories. + +package protocol + +import ( + "github.com/coniks-sys/coniks-go/crypto" + "github.com/coniks-sys/coniks-go/crypto/sign" +) + +type directoryHistory struct { + addr string + signKey sign.PublicKey + snapshots map[uint64]*DirSTR + latestSTR *DirSTR +} + +// A ConiksAuditLog maintains the histories +// of all CONIKS directories known to a CONIKS auditor, +// indexing the histories by the hash of a directory's initial +// STR (specifically, the hash of the STR's signature). +// Each history includes the directory's domain addr as a string, its +// public signing key enabling the auditor to verify the corresponding +// signed tree roots, and a list with all observed snapshots in +// 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 +} + +// 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 +} + +// NewAuditLog constructs a new ConiksAuditLog. It creates an empty +// log; the auditor will add an entry for each CONIKS directory +// the first time it observes an STR for that directory. +func NewAuditLog() ConiksAuditLog { + return make(map[[crypto.HashSizeByte]byte]*directoryHistory) +} + +// set associates the given directoryHistory with the directory identifier +// (i.e. the hash of the initial STR) dirInitHash in the ConiksAuditLog. +func (l ConiksAuditLog) set(dirInitHash [crypto.HashSizeByte]byte, dirHistory *directoryHistory) { + l[dirInitHash] = dirHistory +} + +// get retrieves the directory history for the given directory identifier +// dirInitHash from the ConiksAuditLog. +// Get() also returns a boolean indicating whether the requested dirInitHash +// is present in the log. +func (l ConiksAuditLog) get(dirInitHash [crypto.HashSizeByte]byte) (*directoryHistory, bool) { + h, ok := l[dirInitHash] + return h, ok +} + +// Insert creates a new directory history for the key directory addr +// and inserts it into the audit log l. +// The directory history is initialized with the key directory's +// 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. +// Insert() only creates the initial entry in the log for addr. Use Update() +// to insert newly observed STRs for addr in subsequent epochs. +// 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 + } + + // compute the hash of the initial STR + dirInitHash := ComputeDirectoryIdentity(snaps[0]) + + // error if we want to create a new entry for a directory + // we already know + h, ok := l.get(dirInitHash) + if ok { + return ErrAuditLog + } + + // create the new directory history + h = newDirectoryHistory(addr, signKey, snaps[0]) + + // 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 + // (i.e. snaps is missing an epoch between 0 and the latest given) + for i := 1; i < len(snaps); i++ { + str := snaps[i] + if str == nil { + return ErrMalformedDirectoryMessage + } + + // verify the consistency of each new STR before inserting + // into the audit log + if err := verifySTRConsistency(signKey, h.latestSTR, str); err != nil { + return err + } + + h.updateLatestSTR(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. +// 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 { + return err + } + + // update the latest STR + h.updateLatestSTR(newSTR) + return nil +} + +// GetObservedSTRs gets a range of observed STRs for the CONIKS directory +// address indicated in the AuditingRequest req received from a +// CONIKS client, and returns a tuple of the form (response, error). +// The response (which also includes the error code) is sent back to +// the client. The returned error is used by the auditor +// for logging purposes. +// +// A request without a directory address, with a StartEpoch or EndEpoch +// greater than the latest observed epoch of this directory, or with +// at StartEpoch > EndEpoch is considered +// malformed and causes GetObservedSTRs() to return a +// message.NewErrorResponse(ErrMalformedClientMessage) tuple. +// GetObservedSTRs() returns a message.NewSTRHistoryRange(strs) tuple. +// strs is a list of STRs for the epoch range [StartEpoch, EndEpoch]; +// if StartEpoch == EndEpoch, the list returned is of length 1. +// If the auditor doesn't have any history entries for the requested CONIKS +// directory, GetObservedSTRs() returns a +// 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 { + return NewErrorResponse(ReqUnknownDirectory), ReqUnknownDirectory + } + + // make sure the request is well-formed + if req.EndEpoch > h.latestSTR.Epoch || req.StartEpoch > req.EndEpoch { + return NewErrorResponse(ErrMalformedClientMessage), + ErrMalformedClientMessage + } + + var strs []*DirSTR + for ep := req.StartEpoch; ep <= req.EndEpoch; ep++ { + str := h.snapshots[ep] + strs = append(strs, str) + } + + return NewSTRHistoryRange(strs) +} diff --git a/protocol/auditlog_test.go b/protocol/auditlog_test.go new file mode 100644 index 0000000..6695b1d --- /dev/null +++ b/protocol/auditlog_test.go @@ -0,0 +1,227 @@ +package protocol + +import ( + "github.com/coniks-sys/coniks-go/crypto" + "testing" +) + +func TestInsertEmptyHistory(t *testing.T) { + // create basic test directory and audit log with 1 STR + _, _, _ = NewTestAuditLog(t, 0) +} + +func TestUpdateHistory(t *testing.T) { + // create basic test directory and audit log with 1 STR + d, aud, hist := NewTestAuditLog(t, 0) + + // update the directory so we can update the audit log + dirInitHash := ComputeDirectoryIdentity(hist[0]) + d.Update() + err := aud.Update(dirInitHash, d.LatestSTR()) + + if err != nil { + t.Fatal("Error updating the server history") + } +} + +func TestInsertPriorHistory(t *testing.T) { + // create basic test directory and audit log with 11 STRs + _, _, _ = NewTestAuditLog(t, 10) +} + +func TestInsertExistingHistory(t *testing.T) { + // create basic test directory and audit log with 1 STR + _, aud, hist := NewTestAuditLog(t, 0) + + // let's make sure that we can't re-insert a new server + // history into our log + err := aud.Insert("test-server", nil, hist) + if err != ErrAuditLog { + t.Fatal("Expected an ErrAuditLog when inserting an existing server history") + } +} + +func TestUpdateUnknownHistory(t *testing.T) { + // create basic test directory and audit log with 1 STR + d, aud, _ := NewTestAuditLog(t, 0) + + // let's make sure that we can't update a history for an unknown + // directory in our log + var unknown [crypto.HashSizeByte]byte + err := aud.Update(unknown, d.LatestSTR()) + if err != ErrAuditLog { + t.Fatal("Expected an ErrAuditLog when updating an unknown server history") + } +} + +func TestUpdateBadNewSTR(t *testing.T) { + // create basic test directory and audit log with 11 STRs + d, aud, hist := NewTestAuditLog(t, 10) + + // compute the hash of the initial STR for later lookups + dirInitHash := ComputeDirectoryIdentity(hist[0]) + + // update the directory a few more times and then try + // to update + d.Update() + d.Update() + + err := aud.Update(dirInitHash, d.LatestSTR()) + if err != CheckBadSTR { + t.Fatal("Expected a CheckBadSTR when attempting update a server history with a bad STR") + } +} + +func TestGetLatestObservedSTR(t *testing.T) { + // create basic test directory and audit log with 1 STR + d, aud, hist := NewTestAuditLog(t, 0) + + // compute the hash of the initial STR for later lookups + dirInitHash := ComputeDirectoryIdentity(hist[0]) + + res, err := aud.GetObservedSTRs(&AuditingRequest{ + DirInitSTRHash: dirInitHash, + StartEpoch: uint64(d.LatestSTR().Epoch), + EndEpoch: uint64(d.LatestSTR().Epoch)}) + if err != ReqSuccess { + t.Fatal("Unable to get latest observed STR") + } + + obs := res.DirectoryResponse.(*STRHistoryRange) + if len(obs.STR) == 0 { + t.Fatal("Expect returned STR to be not nil") + } + if obs.STR[0].Epoch != d.LatestSTR().Epoch { + t.Fatal("Unexpected epoch for returned latest STR") + } +} + +func TestGetObservedSTRInEpoch(t *testing.T) { + // create basic test directory and audit log with 11 STRs + _, aud, hist := NewTestAuditLog(t, 10) + + // compute the hash of the initial STR for later lookups + dirInitHash := ComputeDirectoryIdentity(hist[0]) + + res, err := aud.GetObservedSTRs(&AuditingRequest{ + DirInitSTRHash: dirInitHash, + StartEpoch: uint64(6), + EndEpoch: uint64(8)}) + + if err != ReqSuccess { + t.Fatal("Unable to get latest range of STRs") + } + + obs := res.DirectoryResponse.(*STRHistoryRange) + if len(obs.STR) == 0 { + t.Fatal("Expect returned STR to be not nil") + } + if len(obs.STR) != 3 { + t.Fatal("Expect 3 returned STRs") + } + if obs.STR[0].Epoch != 6 || obs.STR[2].Epoch != 8 { + t.Fatal("Unexpected epoch for returned STRs") + } +} + +func TestGetObservedSTRMultipleEpochs(t *testing.T) { + // create basic test directory and audit log with 2 STRs + d, aud, hist := NewTestAuditLog(t, 1) + + // compute the hash of the initial STR for later lookups + dirInitHash := ComputeDirectoryIdentity(hist[0]) + + // first AuditingRequest + res, err := aud.GetObservedSTRs(&AuditingRequest{ + DirInitSTRHash: dirInitHash, + StartEpoch: uint64(0), + EndEpoch: d.LatestSTR().Epoch}) + + if err != ReqSuccess { + t.Fatal("Unable to get latest range of STRs") + } + + obs := res.DirectoryResponse.(*STRHistoryRange) + if len(obs.STR) != 2 { + t.Fatal("Unexpected number of returned STRs") + } + if obs.STR[0].Epoch != 0 { + t.Fatal("Unexpected initial epoch for returned STR range") + } + if obs.STR[1].Epoch != d.LatestSTR().Epoch { + t.Fatal("Unexpected latest STR epoch for returned STR") + } + + // go to next epoch + d.Update() + err1 := aud.Update(dirInitHash, d.LatestSTR()) + if err1 != nil { + t.Fatal("Error occurred updating audit log after auditing request") + } + + // request the new latest STR + res, err = aud.GetObservedSTRs(&AuditingRequest{ + DirInitSTRHash: dirInitHash, + StartEpoch: d.LatestSTR().Epoch, + EndEpoch: d.LatestSTR().Epoch}) + + if err != ReqSuccess { + t.Fatal("Unable to get new latest STRs") + } + + obs = res.DirectoryResponse.(*STRHistoryRange) + if len(obs.STR) != 1 { + t.Fatal("Unexpected number of new latest STRs") + } + if obs.STR[0].Epoch != d.LatestSTR().Epoch { + t.Fatal("Unexpected new latest STR epoch") + } + +} + +func TestGetObservedSTRUnknown(t *testing.T) { + // create basic test directory and audit log with 11 STRs + d, aud, _ := NewTestAuditLog(t, 10) + + var unknown [crypto.HashSizeByte]byte + _, err := aud.GetObservedSTRs(&AuditingRequest{ + DirInitSTRHash: unknown, + StartEpoch: uint64(d.LatestSTR().Epoch), + EndEpoch: uint64(d.LatestSTR().Epoch)}) + if err != ReqUnknownDirectory { + t.Fatal("Expect ReqUnknownDirectory for latest STR") + } + + _, err = aud.GetObservedSTRs(&AuditingRequest{ + DirInitSTRHash: unknown, + StartEpoch: uint64(6), + EndEpoch: uint64(8)}) + if err != ReqUnknownDirectory { + t.Fatal("Expect ReqUnknownDirectory for older STR") + } + +} + +func TestGetObservedSTRMalformed(t *testing.T) { + // create basic test directory and audit log with 11 STRs + _, aud, hist := NewTestAuditLog(t, 10) + + // compute the hash of the initial STR for later lookups + dirInitHash := ComputeDirectoryIdentity(hist[0]) + + // also test the epoch range + _, err := aud.GetObservedSTRs(&AuditingRequest{ + DirInitSTRHash: dirInitHash, + StartEpoch: uint64(6), + EndEpoch: uint64(4)}) + if err != ErrMalformedClientMessage { + t.Fatal("Expect ErrMalformedClientMessage for bad end epoch") + } + _, err = aud.GetObservedSTRs(&AuditingRequest{ + DirInitSTRHash: dirInitHash, + StartEpoch: uint64(6), + EndEpoch: uint64(11)}) + if err != ErrMalformedClientMessage { + t.Fatal("Expect ErrMalformedClientMessage for out-of-bounds epoch range") + } +} diff --git a/protocol/common.go b/protocol/common.go new file mode 100644 index 0000000..01f277b --- /dev/null +++ b/protocol/common.go @@ -0,0 +1,20 @@ +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/common_test.go b/protocol/common_test.go new file mode 100644 index 0000000..8697335 --- /dev/null +++ b/protocol/common_test.go @@ -0,0 +1,41 @@ +package protocol + +import ( + "testing" + + "github.com/coniks-sys/coniks-go/crypto" +) + +func TestComputeDirectoryIdentity(t *testing.T) { + d, _ := NewTestDirectory(t, true) + // str0 := d.LatestSTR() + d.Update() + str1 := d.LatestSTR() + var unknown [crypto.HashSizeByte]byte + type args struct { + str *DirSTR + } + tests := []struct { + name string + args args + want [crypto.HashSizeByte]byte + }{ + // {"normal", args{str0}, ""}, + {"panic", args{str1}, unknown}, + } + for _, tt := range tests { + // FIXME: Refactor testing. See #18. + t.Run(tt.name, func(t *testing.T) { + if tt.name == "panic" { + defer func() { + if r := recover(); r == nil { + t.Errorf("The code did not panic") + } + }() + } + if got := ComputeDirectoryIdentity(tt.args.str); got != tt.want { + t.Errorf("ComputeDirectoryIdentity() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/protocol/consistencychecks.go b/protocol/consistencychecks.go index 856b706..87d6141 100644 --- a/protocol/consistencychecks.go +++ b/protocol/consistencychecks.go @@ -2,6 +2,7 @@ // on data received from a CONIKS directory. // These include data binding proof verification, // and non-equivocation checks. +// TODO: move all STR-verifying functionality to a separate module package protocol @@ -110,14 +111,11 @@ func (cc *ConsistencyChecks) updateSTR(requestType int, msg *Response) error { cc.SavedSTR = str return nil } - // 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 if err := cc.verifySTR(str); err == nil { return nil } // Otherwise, expect that we've entered a new epoch - if err := cc.verifySTRConsistency(cc.SavedSTR, str); err != nil { + if err := verifySTRConsistency(cc.signKey, cc.SavedSTR, str); err != nil { return err } @@ -132,6 +130,12 @@ 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 +// 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 @@ -140,12 +144,13 @@ func (cc *ConsistencyChecks) verifySTR(str *DirSTR) error { } // verifySTRConsistency checks the consistency between 2 snapshots. -// It uses the pinned signing key in cc -// to verify the STR's signature and should not verify -// the hash chain using the STR stored in cc. -func (cc *ConsistencyChecks) verifySTRConsistency(savedSTR, str *DirSTR) error { +// 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 !cc.signKey.Verify(str.Serialize(), str.Signature) { + if !signKey.Verify(str.Serialize(), str.Signature) { return CheckBadSignature } if str.VerifyHashChain(savedSTR) { diff --git a/protocol/error.go b/protocol/error.go index 866d508..be16002 100644 --- a/protocol/error.go +++ b/protocol/error.go @@ -8,18 +8,23 @@ package protocol // An ErrorCode implements the built-in error interface type. type ErrorCode int -// These codes indicate the status of a client-server message exchange. +// These codes indicate the status of a client-server or client-auditor message +// exchange. // Codes prefixed by "Req" indicate different client request results. -// Codes prefixed by "Err" indicate an internal server error or a malformed +// Codes prefixed by "Err" indicate an internal server/auditor error or a malformed // message. const ( ReqSuccess ErrorCode = iota + 100 ReqNameExisted ReqNameNotFound + // auditor->client: no observed history for the requested directory + ReqUnknownDirectory ErrDirectory + ErrAuditLog ErrMalformedClientMessage ErrMalformedDirectoryMessage + ErrMalformedAuditorMessage ) // These codes indicate the result @@ -46,7 +51,9 @@ const ( var Errors = map[ErrorCode]bool{ ErrMalformedClientMessage: true, ErrDirectory: true, + ErrAuditLog: true, ErrMalformedDirectoryMessage: true, + ErrMalformedAuditorMessage: true, } var ( @@ -57,7 +64,9 @@ var ( ErrMalformedClientMessage: "[coniks] Malformed client message", ErrDirectory: "[coniks] Directory error", + ErrAuditLog: "[coniks] Audit log error", ErrMalformedDirectoryMessage: "[coniks] Malformed directory message", + ErrMalformedAuditorMessage: "[coniks] Malformed auditor message", CheckPassed: "[coniks] Consistency checks passed", CheckBadSignature: "[coniks] Directory's signature on STR or TB is invalid", diff --git a/protocol/message.go b/protocol/message.go index c666568..bd3d58c 100644 --- a/protocol/message.go +++ b/protocol/message.go @@ -4,7 +4,10 @@ package protocol -import m "github.com/coniks-sys/coniks-go/merkletree" +import ( + "github.com/coniks-sys/coniks-go/crypto" + m "github.com/coniks-sys/coniks-go/merkletree" +) // The types of requests CONIKS clients send during the CONIKS protocols. const ( @@ -12,6 +15,7 @@ const ( KeyLookupType KeyLookupInEpochType MonitoringType + AuditType ) // A Request message defines the data a CONIKS client must send to a CONIKS @@ -90,6 +94,20 @@ type MonitoringRequest struct { EndEpoch uint64 } +// An AuditingRequest is a message with a CONIKS key directory's address +// as a string, and a StartEpoch and an EndEpoch as uint64's that a CONIKS +// client sends to a CONIKS auditor to request the given directory's +// STRs for the given epoch range. To obtain a single STR, the client +// must set StartEpoch = EndEpoch in the request. +// +// The response to a successful request is an STRHistoryRange with +// a list of STRs covering the epoch range [StartEpoch, EndEpoch]. +type AuditingRequest struct { + DirInitSTRHash [crypto.HashSizeByte]byte + StartEpoch uint64 + EndEpoch uint64 +} + // A Response message indicates the result of a CONIKS client request // with an appropriate error code, and defines the set of cryptographic // proofs a CONIKS directory must return as part of its response. @@ -99,7 +117,7 @@ type Response struct { } // A DirectoryResponse is a message that includes cryptographic proofs -// about the key directory that a CONIKS key directory returns +// about the key directory that a CONIKS key directory or auditor returns // to a CONIKS client. type DirectoryResponse interface{} @@ -124,8 +142,17 @@ type DirectoryProofs struct { STR []*DirSTR } +// An STRHistoryRange response includes a list of signed tree roots +// STR representing a range of the STR hash chain. If the range only +// covers the latest epoch, the list only contains a single STR. +// A CONIKS auditor returns this DirectoryResponse type upon an +// AudutingRequest. +type STRHistoryRange struct { + STR []*DirSTR +} + // NewErrorResponse creates a new response message indicating the error -// that occurred while a CONIKS directory was +// that occurred while a CONIKS directory or a CONIKS auditor was // processing a client request. func NewErrorResponse(e ErrorCode) *Response { return &Response{Error: e} @@ -133,6 +160,7 @@ func NewErrorResponse(e ErrorCode) *Response { var _ DirectoryResponse = (*DirectoryProof)(nil) var _ DirectoryResponse = (*DirectoryProofs)(nil) +var _ DirectoryResponse = (*STRHistoryRange)(nil) // NewRegistrationProof creates the response message a CONIKS directory // sends to a client upon a RegistrationRequest, @@ -216,6 +244,23 @@ func NewMonitoringProof(ap []*m.AuthenticationPath, }, ReqSuccess } +// NewSTRHistoryRange creates the response message a CONIKS auditor +// sends to a client upon an AuditingRequest, +// and returns a Response containing an STRHistoryRange struct. +// auditlog.GetObservedSTRs() passes a list of one or more signed tree roots +// that the auditor observed for the requested range of epochs str. +// +// See auditlog.GetObservedSTRs() for details on the contents of the created +// STRHistoryRange. +func NewSTRHistoryRange(str []*DirSTR) (*Response, ErrorCode) { + return &Response{ + Error: ReqSuccess, + DirectoryResponse: &STRHistoryRange{ + STR: str, + }, + }, ReqSuccess +} + func (msg *Response) validate() error { if Errors[msg.Error] { return msg.Error @@ -229,6 +274,13 @@ func (msg *Response) validate() error { case *DirectoryProofs: // TODO: also do above assertions here return nil + case *STRHistoryRange: + // treat the STRHistoryRange as an auditor response + // bc validate is only called by a client + if len(df.STR) == 0 { + return ErrMalformedAuditorMessage + } + return nil default: panic("[coniks] Malformed response") } diff --git a/protocol/testutil.go b/protocol/testutil.go index 4e464ba..f460222 100644 --- a/protocol/testutil.go +++ b/protocol/testutil.go @@ -14,6 +14,7 @@ import ( func NewTestDirectory(t *testing.T, useTBs bool) ( *ConiksDirectory, sign.PublicKey) { + // FIXME: NewTestDirectory should use a fixed VRF and Signing keys. vrfKey, err := vrf.GenerateKey(nil) if err != nil { t.Fatal(err) @@ -28,3 +29,29 @@ func NewTestDirectory(t *testing.T, useTBs bool) ( d := NewDirectory(1, vrfKey, signKey, 10, useTBs) return d, pk } + +// NewTestAuditLog creates a ConiksAuditLog and corresponding +// ConiksDirectory used for testing auditor-side CONIKS operations. +// The new audit log can be initialized with the number of epochs +// indicating the length of the directory history with which to +// initialize the log; if numEpochs > 0, the history contains numEpochs+1 +// STRs as it always includes the STR after the last directory update +func NewTestAuditLog(t *testing.T, numEpochs int) (*ConiksDirectory, ConiksAuditLog, []*DirSTR) { + d, pk := NewTestDirectory(t, true) + aud := NewAuditLog() + + var hist []*DirSTR + for ep := 0; ep < numEpochs; ep++ { + hist = append(hist, d.LatestSTR()) + d.Update() + } + // always include the actual latest STR + hist = append(hist, d.LatestSTR()) + + err := aud.Insert("test-server", pk, hist) + if err != nil { + t.Fatalf("Error inserting a new history with %d STRs", numEpochs+1) + } + + return d, aud, hist +}