diff --git a/.gitignore b/.gitignore index 95d428a..6b97539 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,13 @@ _testmain.go *.prof # vscode -.vscode \ No newline at end of file +.vscode + +# CONIKS server +config.toml +sign.priv +vrf.priv +server.key +server.pem +coniks.db/ +coniks.pid diff --git a/.travis.yml b/.travis.yml index 5f37f9c..00f3d2c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,6 +21,7 @@ script: - go test -coverprofile=merkletree.coverprofile ./merkletree - go test -coverprofile=utils.coverprofile ./utils - go test -coverprofile=protocol.coverprofile ./protocol + - go test -coverprofile=keyserver.coverprofile ./keyserver after_success: - gover diff --git a/keyserver/README.md b/keyserver/README.md new file mode 100644 index 0000000..6d59e4d --- /dev/null +++ b/keyserver/README.md @@ -0,0 +1,42 @@ +# CONIKS Server implementation in Golang + +## Usage +``` +⇒ go install github.com/coniks-sys/coniks-go/keyserver/coniksserver +⇒ coniksserver -h + _______ _______ __ _ ___ ___ _ _______ +| || || | | || || | | || | +| || _ || |_| || || |_| || _____| +| || | | || || || _|| |_____ +| _|| |_| || _ || || |_ |_____ | +| |_ | || | | || || _ | _____| | +|_______||_______||_| |__||___||___| |_||_______| + +Usage: + coniksserver [command] + +Available Commands: + init Create a configuration file and generate all keys + run Run a CONIKS server instance + +Flags: + -h, --help help for coniksserver + +Use "coniksserver [command] --help" for more information about a command. +``` + +Run the server +``` +⇒ mkdir coniks; cd coniks +⇒ coniksserver init -c # creates all files including a self-signed ssl keys/cert +⇒ coniksserver run -p # run & write down the process ID into coniks.pid +``` + +You can reload the server's policies while it's running by editing the `config.toml` file +and possibly replace `vrf.priv` with a new key, then run +``` +⇒ kill -USR2 `cat coniks.pid` +``` + +## Disclaimer +Please keep in mind that this CONIKS server implementation is under active development. The repository may contain experimental features that aren't fully tested. diff --git a/keyserver/coniksserver/cmd/init.go b/keyserver/coniksserver/cmd/init.go new file mode 100644 index 0000000..819d0ec --- /dev/null +++ b/keyserver/coniksserver/cmd/init.go @@ -0,0 +1,105 @@ +package cmd + +import ( + "bytes" + "io/ioutil" + "log" + "os" + "path" + "strconv" + + "github.com/BurntSushi/toml" + "github.com/coniks-sys/coniks-go/crypto/sign" + "github.com/coniks-sys/coniks-go/crypto/vrf" + "github.com/coniks-sys/coniks-go/keyserver" + "github.com/coniks-sys/coniks-go/keyserver/testutil" + "github.com/coniks-sys/coniks-go/utils" + "github.com/spf13/cobra" +) + +// initCmd represents the init command +var initCmd = &cobra.Command{ + Use: "init", + Short: "Create a configuration file and generate all keys", + Long: `Create a configuration file and generate all keys for signing and VRF`, + Run: func(cmd *cobra.Command, args []string) { + dir := cmd.Flag("dir").Value.String() + mkConfig(dir) + mkSigningKey(dir) + mkVrfKey(dir) + + cert, err := strconv.ParseBool(cmd.Flag("cert").Value.String()) + if err == nil && cert { + testutil.CreateTLSCert(dir) + } + }, +} + +func init() { + RootCmd.AddCommand(initCmd) + initCmd.Flags().StringP("dir", "d", ".", "Location of directory for storing generated files") + initCmd.Flags().BoolP("cert", "c", false, "Generate self-signed ssl keys/cert with sane defaults") +} + +func mkConfig(dir string) { + file := path.Join(dir, "config.toml") + var conf = keyserver.ServerConfig{ + DatabasePath: "coniks.db", + LoadedHistoryLength: 1000000, + TLS: &keyserver.TLSConnection{ + LocalAddress: "127.0.0.1:3001", + PublicAddress: "0.0.0.0:3000", + TLSCertPath: "server.pem", + TLSKeyPath: "server.key", + }, + Policies: &keyserver.ServerPolicies{ + EpochDeadline: 60, + VRFKeyPath: "vrf.priv", + SignKeyPath: "sign.priv", + }, + } + + var confBuf bytes.Buffer + + e := toml.NewEncoder(&confBuf) + err := e.Encode(conf) + if err != nil { + log.Println(err) + return + } + util.WriteFile(file, confBuf) +} + +func mkSigningKey(dir string) { + file := path.Join(dir, "sign.priv") + if _, err := os.Stat(file); err == nil { + log.Printf("%s already exists\n", file) + return + } + sk, err := sign.GenerateKey(nil) + if err != nil { + log.Print(err) + return + } + if err := ioutil.WriteFile(file, sk[:], 0600); err != nil { + log.Print(err) + return + } +} + +func mkVrfKey(dir string) { + file := path.Join(dir, "vrf.priv") + if _, err := os.Stat(file); err == nil { + log.Printf("%s already exists\n", file) + return + } + sk, err := vrf.GenerateKey(nil) + if err != nil { + log.Print(err) + return + } + if err := ioutil.WriteFile(file, sk[:], 0600); err != nil { + log.Print(err) + return + } +} diff --git a/keyserver/coniksserver/cmd/root.go b/keyserver/coniksserver/cmd/root.go new file mode 100644 index 0000000..a0cc421 --- /dev/null +++ b/keyserver/coniksserver/cmd/root.go @@ -0,0 +1,32 @@ +package cmd + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" +) + +// RootCmd represents the base command when called without any subcommands +var RootCmd = &cobra.Command{ + Use: "coniksserver", + Short: "CONIKS reference implementation in Go", + Long: ` +________ _______ __ _ ___ ___ _ _______ +| || || | | || || | | || | +| || _ || |_| || || |_| || _____| +| || | | || || || _|| |_____ +| _|| |_| || _ || || |_ |_____ | +| |_ | || | | || || _ | _____| | +|_______||_______||_| |__||___||___| |_||_______| +`, +} + +// Execute adds all child commands to the root command sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + if err := RootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(-1) + } +} diff --git a/keyserver/coniksserver/cmd/run.go b/keyserver/coniksserver/cmd/run.go new file mode 100644 index 0000000..b0fc3c1 --- /dev/null +++ b/keyserver/coniksserver/cmd/run.go @@ -0,0 +1,65 @@ +package cmd + +import ( + "fmt" + "log" + "os" + "os/signal" + "path" + "strconv" + + "github.com/coniks-sys/coniks-go/keyserver" + "github.com/spf13/cobra" +) + +// runCmd represents the run command +var runCmd = &cobra.Command{ + Use: "run", + Short: "Run a CONIKS server instance", + Long: `Run a CONIKS server instance + +This will look for config files with default names (config.toml) +in the current directory if not specified differently. + `, + Run: func(cmd *cobra.Command, args []string) { + config := cmd.Flag("config").Value.String() + pid, _ := strconv.ParseBool(cmd.Flag("pid").Value.String()) + // ignore the error here since it is handled by the flag parser. + if pid { + writePID() + } + run(config) + }, +} + +func init() { + RootCmd.AddCommand(runCmd) + runCmd.Flags().StringP("config", "c", "config.toml", "Path to server configuration file") + runCmd.Flags().BoolP("pid", "p", false, "Write down the process id to coniks.pid in the current working directory") +} + +func run(confPath string) { + conf, err := keyserver.LoadServerConfig(confPath) + if err != nil { + log.Fatal(err) + } + serv := keyserver.NewConiksServer(conf) + + // run the server until receiving an interrupt signal + serv.Run(conf.TLS) + ch := make(chan os.Signal, 1) + signal.Notify(ch, os.Interrupt) + <-ch + serv.Shutdown() +} + +func writePID() { + pidf, err := os.OpenFile(path.Join(".", "coniks.pid"), os.O_CREATE|os.O_WRONLY, 0666) + if err != nil { + log.Printf("Cannot create coniks.pid: %v", err) + return + } + if _, err := fmt.Fprint(pidf, os.Getpid()); err != nil { + log.Printf("Cannot write to pid file: %v", err) + } +} diff --git a/keyserver/coniksserver/coniksserver.go b/keyserver/coniksserver/coniksserver.go new file mode 100644 index 0000000..dc5f993 --- /dev/null +++ b/keyserver/coniksserver/coniksserver.go @@ -0,0 +1,7 @@ +package main + +import "github.com/coniks-sys/coniks-go/keyserver/coniksserver/cmd" + +func main() { + cmd.Execute() +} diff --git a/keyserver/encoding.go b/keyserver/encoding.go new file mode 100644 index 0000000..68494dc --- /dev/null +++ b/keyserver/encoding.go @@ -0,0 +1,34 @@ +// Defines methods/functions to encode/decode messages between client and server. +// Currently this module supports JSON marshal/unmarshal only. +// Protobuf would be supported in the feature. + +package keyserver + +import ( + "encoding/json" + + . "github.com/coniks-sys/coniks-go/protocol" +) + +func MarshalResponse(response Response) ([]byte, error) { + return json.Marshal(response) +} + +func UnmarshalRequest(msg []byte) (*Request, error) { + var content json.RawMessage + req := Request{ + Request: &content, + } + if err := json.Unmarshal(msg, &req); err != nil { + return nil, err + } + switch req.Type { + case RegistrationType: + var request RegistrationRequest + if err := json.Unmarshal(content, &request); err != nil { + return nil, err + } + req.Request = &request + } + return &req, nil +} diff --git a/keyserver/listener.go b/keyserver/listener.go new file mode 100644 index 0000000..afd6a6e --- /dev/null +++ b/keyserver/listener.go @@ -0,0 +1,114 @@ +package keyserver + +import ( + "crypto/tls" + "io" + "log" + "net" + "time" + + . "github.com/coniks-sys/coniks-go/protocol" +) + +func (server *ConiksServer) listenForRequests(ln *net.TCPListener, handler func(msg []byte) ([]byte, error)) { + defer ln.Close() + go func() { + <-server.stop + ln.SetDeadline(time.Now()) + }() + + for { + select { + case <-server.stop: + server.waitCloseConn.Wait() + return + default: + } + conn, err := ln.Accept() + if err != nil { + if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() { + continue + } + log.Printf("accept client: %s", err) + continue + } + conn = tls.Server(conn, server.tlsConfig) + server.waitCloseConn.Add(1) + go func() { + server.acceptClient(conn, handler) + server.waitCloseConn.Done() + }() + } +} + +func (server *ConiksServer) acceptClient(conn net.Conn, handler func(msg []byte) ([]byte, error)) { + defer conn.Close() + + conn.SetDeadline(time.Now().Add(5 * time.Second)) + + // handle request + buf := make([]byte, 4096) + for { + n, err := conn.Read(buf) + if err != nil { + if err != io.EOF { + log.Printf("client read %v: %v", conn.RemoteAddr(), err) + } + return + } + + res, err := handler(buf[:n]) + if err != nil { + log.Printf("client handle %v: %v", conn.RemoteAddr(), err) + } + + n, err = conn.Write([]byte(res)) + if err != nil { + log.Printf("client write %v: %v", conn.RemoteAddr(), err) + return + } + } +} + +func malformedClientMsg(err error) ([]byte, error) { + // check if we're just propagating a message + if err == nil { + err = ErrorMalformedClientMessage.Error() + } + response := NewErrorResponse(ErrorMalformedClientMessage) + res, e := MarshalResponse(response) + if e != nil { + panic(e) + } + return res, err +} + +func (server *ConiksServer) makeHandler(acceptableTypes map[int]bool) func(msg []byte) ([]byte, error) { + return func(msg []byte) ([]byte, error) { + // get request message + req, err := UnmarshalRequest(msg) + if err != nil { + return malformedClientMsg(err) + } + if !acceptableTypes[req.Type] { + log.Printf("unacceptable message type: %q", req.Type) + return malformedClientMsg(ErrorMalformedClientMessage.Error()) + } + + switch req.Type { + default: + server.Lock() + } + response, e := server.dir.HandleOps(req) + switch req.Type { + default: + server.Unlock() + } + + res, err := MarshalResponse(response) + if err != nil { + panic(err) + } + return res, e.Error() + } +} diff --git a/keyserver/server.go b/keyserver/server.go new file mode 100644 index 0000000..2ed6f50 --- /dev/null +++ b/keyserver/server.go @@ -0,0 +1,224 @@ +package keyserver + +import ( + "crypto/tls" + "fmt" + "io/ioutil" + "log" + "net" + "os" + "os/signal" + "sync" + "syscall" + "time" + + "github.com/BurntSushi/toml" + "github.com/coniks-sys/coniks-go/crypto/sign" + "github.com/coniks-sys/coniks-go/crypto/vrf" + "github.com/coniks-sys/coniks-go/merkletree" + "github.com/coniks-sys/coniks-go/protocol" + "github.com/coniks-sys/coniks-go/storage/kv" + "github.com/coniks-sys/coniks-go/storage/kv/leveldbkv" + "github.com/coniks-sys/coniks-go/utils" +) + +type ServerConfig struct { + configFilePath string + DatabasePath string `toml:"database"` + LoadedHistoryLength uint64 `toml:"loaded_history_length"` + Policies *ServerPolicies `toml:"policies"` + TLS *TLSConnection `toml:"tls"` +} + +type TLSConnection struct { + PublicAddress string `toml:"public_address"` // address:port + LocalAddress string `toml:"local_address"` + TLSCertPath string `toml:"cert"` + TLSKeyPath string `toml:"key"` +} + +type ServerPolicies struct { + EpochDeadline merkletree.TimeStamp `toml:"epoch_deadline"` + VRFKeyPath string `toml:"vrf_key_path"` + SignKeyPath string `toml:"sign_key_path"` // it should be a part of policies, see #47 + vrfKey vrf.PrivateKey + signKey sign.PrivateKey +} + +type ConiksServer struct { + sync.RWMutex + dir *protocol.ConiksDirectory + + stop chan struct{} + waitStop sync.WaitGroup + waitCloseConn sync.WaitGroup + + db kv.DB // TODO: it is a placeholer for issue #37 + + configFilePath string + reloadChan chan os.Signal + epochTimer *time.Timer + tlsConfig *tls.Config +} + +func LoadServerConfig(file string) (*ServerConfig, error) { + var conf ServerConfig + if _, err := toml.DecodeFile(file, &conf); err != nil { + return nil, fmt.Errorf("Failed to load config: %v", err) + } + + // load signing key + signPath := util.ResolvePath(conf.Policies.SignKeyPath, file) + signKey, err := ioutil.ReadFile(signPath) + if err != nil { + return nil, fmt.Errorf("Cannot read signing key: %v", err) + } + if len(signKey) != sign.PrivateKeySize { + return nil, fmt.Errorf("Signing key must be 64 bytes (got %d)", len(signKey)) + } + + // load VRF key + vrfPath := util.ResolvePath(conf.Policies.VRFKeyPath, file) + vrfKey, err := ioutil.ReadFile(vrfPath) + if err != nil { + return nil, fmt.Errorf("Cannot read VRF key: %v", err) + } + if len(vrfKey) != vrf.PrivateKeySize { + return nil, fmt.Errorf("VRF key must be 64 bytes (got %d)", len(vrfKey)) + } + + conf.configFilePath = file + conf.Policies.vrfKey = vrfKey + conf.Policies.signKey = signKey + // also update path for db & TLS cert files + conf.DatabasePath = util.ResolvePath(conf.DatabasePath, file) + conf.TLS.TLSCertPath = util.ResolvePath(conf.TLS.TLSCertPath, file) + conf.TLS.TLSKeyPath = util.ResolvePath(conf.TLS.TLSKeyPath, file) + + return &conf, nil +} + +func NewConiksServer(conf *ServerConfig) *ConiksServer { + // open db + kvdb := leveldbkv.OpenDB(conf.DatabasePath) + + // create server instance + server := new(ConiksServer) + server.dir = protocol.InitDirectory( + conf.Policies.EpochDeadline, + conf.Policies.vrfKey, + conf.Policies.signKey, + conf.LoadedHistoryLength, + true) + server.stop = make(chan struct{}) + server.db = kvdb + server.configFilePath = conf.configFilePath + server.reloadChan = make(chan os.Signal, 1) + signal.Notify(server.reloadChan, syscall.SIGUSR2) + server.epochTimer = time.NewTimer(time.Duration(conf.Policies.EpochDeadline) * time.Second) + + return server +} + +func (server *ConiksServer) Run(tc *TLSConnection) { + server.waitStop.Add(1) + go func() { + server.EpochUpdate() + server.waitStop.Done() + }() + + // server listener + cer, err := tls.LoadX509KeyPair(tc.TLSCertPath, tc.TLSKeyPath) + if err != nil { + panic(err) + } + + server.tlsConfig = &tls.Config{Certificates: []tls.Certificate{cer}} + + addr, err := net.ResolveTCPAddr("tcp", tc.PublicAddress) + if err != nil { + panic(err) + } + + // server public port + publicLn, err := net.ListenTCP("tcp", addr) + if err != nil { + panic(err) + } + + addr, err = net.ResolveTCPAddr("tcp", tc.LocalAddress) + if err != nil { + panic(err) + } + + // server registration port + localLn, err := net.ListenTCP("tcp", addr) + if err != nil { + panic(err) + } + + // acceptable types for public connection + publicTypes := make(map[int]bool) + server.waitStop.Add(1) + go func() { + server.listenForRequests(publicLn, server.makeHandler(publicTypes)) + server.waitStop.Done() + }() + + // acceptable types for local connection + localTypes := make(map[int]bool) + localTypes[protocol.RegistrationType] = true + server.waitStop.Add(1) + go func() { + server.listenForRequests(localLn, server.makeHandler(localTypes)) + server.waitStop.Done() + }() + + server.waitStop.Add(1) + go func() { + server.updatePolicies() + server.waitStop.Done() + }() +} + +func (server *ConiksServer) EpochUpdate() { + for { + select { + case <-server.stop: + return + case <-server.epochTimer.C: + server.Lock() + server.dir.Update() + server.epochTimer.Reset(time.Duration(server.dir.EpochDeadline()) * time.Second) + server.Unlock() + } + } +} + +func (server *ConiksServer) Shutdown() error { + close(server.stop) + server.waitStop.Wait() + return server.db.Close() +} + +func (server *ConiksServer) updatePolicies() { + for { + select { + case <-server.stop: + return + case <-server.reloadChan: + // read server policies from config file + conf, err := LoadServerConfig(server.configFilePath) + if err != nil { + log.Println(err) + // error occured while reading server config + // simply abort the reloading policies process + return + } + server.Lock() + server.dir.SetPolicies(conf.Policies.EpochDeadline, conf.Policies.vrfKey) + server.Unlock() + log.Println("Policies reloaded!") + } + } +} diff --git a/keyserver/server_test.go b/keyserver/server_test.go new file mode 100644 index 0000000..75ade77 --- /dev/null +++ b/keyserver/server_test.go @@ -0,0 +1,239 @@ +package keyserver + +import ( + "bytes" + "encoding/json" + "path" + "syscall" + "testing" + "time" + + "github.com/coniks-sys/coniks-go/crypto/sign" + "github.com/coniks-sys/coniks-go/crypto/vrf" + "github.com/coniks-sys/coniks-go/keyserver/testutil" + "github.com/coniks-sys/coniks-go/merkletree" + . "github.com/coniks-sys/coniks-go/protocol" + "github.com/coniks-sys/coniks-go/storage/kv" + "github.com/coniks-sys/coniks-go/utils" +) + +func startServer(t *testing.T, kvdb kv.DB, epDeadline merkletree.TimeStamp, policiesPath string) (*ConiksServer, func()) { + dir, teardown := testutil.CreateTLSCertForTest(t) + + signKey, err := sign.GenerateKey(nil) + if err != nil { + t.Fatal(err) + } + + vrfKey, err := vrf.GenerateKey(nil) + if err != nil { + t.Fatal(err) + } + + conf := &ServerConfig{ + DatabasePath: path.Join(dir, "coniks.db"), + LoadedHistoryLength: 100, + TLS: &TLSConnection{ + PublicAddress: testutil.PublicConnection, + LocalAddress: testutil.LocalConnection, + TLSCertPath: path.Join(dir, "server.pem"), + TLSKeyPath: path.Join(dir, "server.key"), + }, + Policies: &ServerPolicies{ + EpochDeadline: epDeadline, + vrfKey: vrfKey, + signKey: signKey, + }, + } + + server := NewConiksServer(conf) + server.db = kvdb + server.Run(conf.TLS) + return server, func() { + server.Shutdown() + teardown() + } +} + +func TestServerStartStop(t *testing.T) { + util.WithDB(func(db kv.DB) { + _, teardown := startServer(t, db, 60, "") + defer teardown() + }) +} + +func TestServerReloadPoliciesWithError(t *testing.T) { + util.WithDB(func(db kv.DB) { + server, teardown := startServer(t, db, 1, "") + defer teardown() + syscall.Kill(syscall.Getpid(), syscall.SIGUSR2) + if server.dir.EpochDeadline() != 1 { + t.Fatal("Expect the server's policies not change") + } + // just to make sure the server's still running normally + timer := time.NewTimer(1 * time.Second) + <-timer.C + }) +} + +func TestBotSendsRegistration(t *testing.T) { + util.WithDB(func(db kv.DB) { + _, teardown := startServer(t, db, 60, "") + defer teardown() + reg := ` +{ + "type": 0, + "request": { + "username": "alice@twitter", + "key": [0,1,2], + "allow_unsigned_key_change": true, + "allow_public_lookup": true + } +} +` + rev, err := testutil.NewClient(t, testutil.LocalConnection, []byte(reg)) + if err != nil { + t.Fatal(err) + } + + type ExpectingResponse struct { + Type int + Error ErrorCode + Content json.RawMessage + } + var response ExpectingResponse + err = json.Unmarshal(rev, &response) + if err != nil { + t.Log(string(rev)) + t.Fatal(err) + } + if response.Type != RegistrationType { + t.Fatal("Expect a registration response") + } + if response.Error != Success { + t.Fatal("Expect a successful registration", "got", response.Error) + } + }) +} + +func TestSendsRegistrationFromOutside(t *testing.T) { + util.WithDB(func(db kv.DB) { + _, teardown := startServer(t, db, 60, "") + defer teardown() + reg := ` +{ + "type": 0, + "request": { + "username": "alice@twitter", + "key": [0,1,2], + "allow_unsigned_key_change": true, + "allow_public_lookup": true + } +} +` + rev, err := testutil.NewClient(t, testutil.PublicConnection, []byte(reg)) + if err != nil { + t.Fatal(err) + } + var response ErrorResponse + err = json.Unmarshal(rev, &response) + if err != nil { + t.Fatal(err) + } + if response.Error != ErrorMalformedClientMessage { + t.Fatalf("Expect error code %d", ErrorMalformedClientMessage) + } + }) +} + +func TestUpdateDirectory(t *testing.T) { + util.WithDB(func(db kv.DB) { + server, teardown := startServer(t, db, 1, "") + defer teardown() + str0 := server.dir.LatestSTR() + rs := createMultiRegistrationRequests(10) + for i := range rs { + _, err := server.dir.HandleOps(rs[i]) + if err != Success { + t.Fatal("Error while submitting registration request number", i, "to server") + } + } + timer := time.NewTimer(1 * time.Second) + <-timer.C + str1 := server.dir.LatestSTR() + if str0.Epoch != 0 || str1.Epoch != 1 || !merkletree.VerifyHashChain(str1.PreviousSTRHash, str0.Signature) { + t.Fatal("Expect next STR in hash chain") + } + }) +} + +func createMultiRegistrationRequests(N uint64) []*Request { + var rs []*Request + for i := uint64(0); i < N; i++ { + r := &Request{ + Type: RegistrationType, + Request: &RegistrationRequest{ + Username: "user" + string(i), + Key: []byte("key" + string(i)), + AllowPublicLookup: true, + AllowUnsignedKeychange: true, + }, + } + rs = append(rs, r) + } + return rs +} + +func TestRegisterDuplicateUserInOneEpoch(t *testing.T) { + util.WithDB(func(db kv.DB) { + server, teardown := startServer(t, db, 60, "") + defer teardown() + r0 := createMultiRegistrationRequests(1)[0] + r1 := createMultiRegistrationRequests(1)[0] + _, err := server.dir.HandleOps(r0) + if err != Success { + t.Fatal("Error while submitting registration request") + } + rev, err := server.dir.HandleOps(r1) + response, ok := rev.(*DirectoryProof) + if !ok { + t.Fatal("Expect a directory proof response") + } + if err != ErrorNameExisted || + response.Error != ErrorNameExisted { + t.Fatal("Expect error code", ErrorNameExisted) + } + if response.STR == nil || response.AP == nil || response.TB == nil { + t.Fatal("Unexpected response") + } + if !bytes.Equal(response.TB.Value, r1.Request.(*RegistrationRequest).Key) { + t.Fatal("Unexpect returned TB") + } + }) +} + +func TestRegisterDuplicateUserInDifferentEpoches(t *testing.T) { + util.WithDB(func(db kv.DB) { + server, teardown := startServer(t, db, 1, "") + defer teardown() + r0 := createMultiRegistrationRequests(1)[0] + _, err := server.dir.HandleOps(r0) + if err != Success { + t.Fatal("Error while submitting registration request") + } + timer := time.NewTimer(2 * time.Second) + <-timer.C + rev, err := server.dir.HandleOps(r0) + response, ok := rev.(*DirectoryProof) + if !ok { + t.Fatal("Expect a directory proof response") + } + if err != ErrorNameExisted || + response.Error != ErrorNameExisted { + t.Fatal("Expect error code", ErrorNameExisted, "got", err) + } + if response.STR == nil || response.AP == nil || response.TB != nil { + t.Fatal("Unexpected response") + } + }) +} diff --git a/keyserver/testutil/testutil.go b/keyserver/testutil/testutil.go new file mode 100644 index 0000000..bde5283 --- /dev/null +++ b/keyserver/testutil/testutil.go @@ -0,0 +1,120 @@ +package testutil + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "io/ioutil" + "math/big" + "net" + "os" + "path" + "testing" + "time" +) + +const ( + TestDir = "coniksServerTest" + PublicConnection = "127.0.0.1:3000" + LocalConnection = "127.0.0.1:3001" +) + +func CreateTLSCert(dir string) error { + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return err + } + + notBefore := time.Now() + notAfter := notBefore.Add(1 * time.Hour) + + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return err + } + + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{"Coniks.org"}, + }, + NotBefore: notBefore, + NotAfter: notAfter, + + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + IsCA: true, + } + template.Subject.CommonName = "localhost" + template.IPAddresses = append(template.IPAddresses, net.ParseIP("127.0.0.1")) + + derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) + if err != nil { + return err + } + + certOut, err := os.Create(path.Join(dir, "server.pem")) + if err != nil { + return err + } + pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + certOut.Close() + + keyOut, err := os.OpenFile(path.Join(dir, "server.key"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return err + } + + b, err := x509.MarshalECPrivateKey(priv) + if err != nil { + return err + } + pemBlock := &pem.Block{Type: "EC PRIVATE KEY", Bytes: b} + pem.Encode(keyOut, pemBlock) + keyOut.Close() + return nil +} + +func CreateTLSCertForTest(t *testing.T) (string, func()) { + dir, err := ioutil.TempDir("", TestDir) + if err != nil { + t.Fatal(err) + } + err = CreateTLSCert(dir) + if err != nil { + t.Fatal(err) + } + return dir, func() { + os.RemoveAll(dir) + } +} + +func NewClient(t *testing.T, address string, msg []byte) ([]byte, error) { + conf := &tls.Config{ + InsecureSkipVerify: true, + } + + conn, err := tls.Dial("tcp", address, conf) + if err != nil { + return nil, err + } + defer conn.Close() + _, err = conn.Write([]byte(msg)) + if err != nil { + return nil, err + } + + buf := make([]byte, 4096) + n, err := conn.Read(buf) + if err != nil { + return nil, err + } + + return buf[:n], nil +} diff --git a/merkletree/policy.go b/merkletree/policy.go index c6a858f..957a526 100644 --- a/merkletree/policy.go +++ b/merkletree/policy.go @@ -9,6 +9,7 @@ import ( type TimeStamp uint64 type Policies interface { + EpDeadline() TimeStamp Serialize() []byte vrfPrivate() vrf.PrivateKey } @@ -55,3 +56,7 @@ func (p *ConiksPolicies) Serialize() []byte { func (p *ConiksPolicies) vrfPrivate() vrf.PrivateKey { return p.vrfPrivateKey } + +func (p *ConiksPolicies) EpDeadline() TimeStamp { + return p.EpochDeadline +} diff --git a/protocol/directory.go b/protocol/directory.go index 69ca775..be16d4a 100644 --- a/protocol/directory.go +++ b/protocol/directory.go @@ -43,6 +43,10 @@ func (d *ConiksDirectory) SetPolicies(epDeadline merkletree.TimeStamp, vrfKey vr d.policies = merkletree.NewPolicies(epDeadline, vrfKey) } +func (d *ConiksDirectory) EpochDeadline() merkletree.TimeStamp { + return d.policies.EpDeadline() +} + func (d *ConiksDirectory) LatestSTR() *merkletree.SignedTreeRoot { return d.pad.LatestSTR() } @@ -54,11 +58,16 @@ func (d *ConiksDirectory) HandleOps(req *Request) (Response, ErrorCode) { case RegistrationType: if msg, ok := req.Request.(*RegistrationRequest); ok { if len(msg.Username) > 0 && len(msg.Key) > 0 { - return d.Register(msg) + res, e := d.Register(msg) + if res == nil { + return NewErrorResponse(e), e + } + return res, e } } } - return nil, ErrorMalformedClientMessage + return NewErrorResponse(ErrorMalformedClientMessage), + ErrorMalformedClientMessage } func (d *ConiksDirectory) Register(req *RegistrationRequest) ( @@ -70,13 +79,13 @@ func (d *ConiksDirectory) Register(req *RegistrationRequest) ( return nil, ErrorDirectory } if bytes.Equal(ap.LookupIndex, ap.Leaf.Index) { - return NewRegistrationProof(ap, d.LatestSTR(), nil), ErrorNameExisted + return NewRegistrationProof(ap, d.LatestSTR(), nil, ErrorNameExisted) } if d.useTBs { // also check the temporary bindings array // currently the server allows only one registration/key change per epoch if tb := d.tbs[req.Username]; tb != nil { - return NewRegistrationProof(ap, d.LatestSTR(), tb), ErrorNameExisted + return NewRegistrationProof(ap, d.LatestSTR(), tb, ErrorNameExisted) } // insert new data to the directory on-the-fly @@ -85,11 +94,11 @@ func (d *ConiksDirectory) Register(req *RegistrationRequest) ( return nil, ErrorDirectory } d.tbs[req.Username] = tb - return NewRegistrationProof(ap, d.LatestSTR(), tb), Success + return NewRegistrationProof(ap, d.LatestSTR(), tb, Success) } else { if err = d.pad.Set(req.Username, req.Key); err != nil { return nil, ErrorDirectory } - return NewRegistrationProof(ap, d.LatestSTR(), nil), Success + return NewRegistrationProof(ap, d.LatestSTR(), nil, Success) } } diff --git a/protocol/error.go b/protocol/error.go index 14b4fee..5661368 100644 --- a/protocol/error.go +++ b/protocol/error.go @@ -17,6 +17,7 @@ const ( var ( errorMessages = map[ErrorCode]error{ + Success: nil, ErrorMalformedClientMessage: errors.New("[coniks] Malformed client request"), ErrorNameExisted: errors.New("[coniks] Registering identity is already registered"), ErrorNameNotFound: errors.New("[coniks] Name not found"), @@ -25,8 +26,5 @@ var ( ) func (e ErrorCode) Error() error { - if errorMessages[e] == nil { - return errorMessages[ErrorDirectory] - } return errorMessages[e] } diff --git a/protocol/message.go b/protocol/message.go index 7624f9d..22ae79b 100644 --- a/protocol/message.go +++ b/protocol/message.go @@ -35,13 +35,14 @@ func NewErrorResponse(e ErrorCode) Response { } type DirectoryProof struct { - Type int - AP *m.AuthenticationPath - STR *m.SignedTreeRoot - TB *m.TemporaryBinding `json:",omitempty"` + Type int + AP *m.AuthenticationPath + STR *m.SignedTreeRoot + TB *m.TemporaryBinding `json:",omitempty"` + Error ErrorCode `json:",omitempty"` } func NewRegistrationProof(ap *m.AuthenticationPath, str *m.SignedTreeRoot, - tb *m.TemporaryBinding) *DirectoryProof { - return &DirectoryProof{RegistrationType, ap, str, tb} + tb *m.TemporaryBinding, e ErrorCode) (*DirectoryProof, ErrorCode) { + return &DirectoryProof{RegistrationType, ap, str, tb, e}, e } diff --git a/utils/util.go b/utils/util.go index fd61646..574d204 100644 --- a/utils/util.go +++ b/utils/util.go @@ -6,6 +6,7 @@ import ( "io/ioutil" "log" "os" + "path/filepath" ) // GetNthBit finds the bit in the byte array bs @@ -60,3 +61,10 @@ func WriteFile(filename string, buf bytes.Buffer) { return } } + +func ResolvePath(file, other string) string { + if !filepath.IsAbs(file) { + file = filepath.Join(filepath.Dir(other), file) + } + return file +}