From dee5a19d36554a8f9a365efd65d13b134889bf63 Mon Sep 17 00:00:00 2001 From: Niall Sheridan Date: Sun, 19 Jun 2016 23:44:25 +0100 Subject: first pass at a certificate store --- cmd/cashierd/main.go | 78 ++++++++++++++++++++++++++++++++++++++++++++-------- cmd/dbinit/dbinit.go | 61 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 12 deletions(-) create mode 100644 cmd/dbinit/dbinit.go (limited to 'cmd') diff --git a/cmd/cashierd/main.go b/cmd/cashierd/main.go index 8f08122..1db7d30 100644 --- a/cmd/cashierd/main.go +++ b/cmd/cashierd/main.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "crypto/rand" "encoding/hex" "encoding/json" @@ -24,9 +25,11 @@ import ( "github.com/nsheridan/cashier/server/auth" "github.com/nsheridan/cashier/server/auth/github" "github.com/nsheridan/cashier/server/auth/google" + "github.com/nsheridan/cashier/server/certutil" "github.com/nsheridan/cashier/server/config" "github.com/nsheridan/cashier/server/fs" "github.com/nsheridan/cashier/server/signer" + "github.com/nsheridan/cashier/server/store" "github.com/nsheridan/cashier/templates" ) @@ -34,12 +37,13 @@ var ( cfg = flag.String("config_file", "cashierd.conf", "Path to configuration file.") ) -// appContext contains local context - cookiestore, authprovider, authsession, templates etc. +// appContext contains local context - cookiestore, authprovider, authsession etc. type appContext struct { cookiestore *sessions.CookieStore authprovider auth.Provider authsession *auth.Session sshKeySigner *signer.KeySigner + certstore store.CertStorer } // getAuthTokenCookie retrieves a cookie from the request. @@ -124,13 +128,16 @@ func signHandler(a *appContext, w http.ResponseWriter, r *http.Request) (int, er if err != nil { return http.StatusInternalServerError, err } - signed, err := a.sshKeySigner.SignUserKey(req) + cert, err := a.sshKeySigner.SignUserKey(req) if err != nil { return http.StatusInternalServerError, err } + if err := a.certstore.SetCert(cert); err != nil { + log.Printf("Error recording cert: %v", err) + } json.NewEncoder(w).Encode(&lib.SignResponse{ Status: "ok", - Response: signed, + Response: certutil.GetPublicKey(cert), }) return http.StatusOK, nil } @@ -174,6 +181,38 @@ func rootHandler(a *appContext, w http.ResponseWriter, r *http.Request) (int, er return http.StatusOK, nil } +func revokedCertsHandler(a *appContext, w http.ResponseWriter, r *http.Request) (int, error) { + var out bytes.Buffer + revoked, err := a.certstore.List() + if err != nil { + return http.StatusInternalServerError, err + } + for _, c := range revoked { + if c.Revoked { + out.WriteString(c.Raw) + out.WriteString("\n") + } + } + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.Write(out.Bytes()) + return http.StatusOK, nil +} + +func revokeCertHandler(a *appContext, w http.ResponseWriter, r *http.Request) (int, error) { + if r.Method == "GET" { + return http.StatusMethodNotAllowed, errors.New(http.StatusText(http.StatusMethodNotAllowed)) + } + r.ParseForm() + id := r.FormValue("cert_id") + if id == "" { + return http.StatusBadRequest, errors.New(http.StatusText(http.StatusBadRequest)) + } + if err := a.certstore.Revoke(r.FormValue("cert_id")); err != nil { + return http.StatusInternalServerError, err + } + return http.StatusOK, nil +} + // appHandler is a handler which uses appContext to manage state. type appHandler struct { *appContext @@ -184,14 +223,8 @@ type appHandler struct { func (ah appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { status, err := ah.h(ah.appContext, w, r) if err != nil { - switch status { - case http.StatusNotFound: - http.NotFound(w, r) - case http.StatusInternalServerError: - http.Error(w, http.StatusText(status), status) - default: - http.Error(w, http.StatusText(status), status) - } + log.Printf("HTTP %d: %q", status, err) + http.Error(w, err.Error(), status) } } @@ -213,6 +246,21 @@ func readConfig(filename string) (*config.Config, error) { return config.ReadConfig(f) } +func certStore(config string) (store.CertStorer, error) { + var cstore store.CertStorer + var err error + engine := strings.Split(config, ":")[0] + switch engine { + case "mysql": + cstore, err = store.NewMySQLStore(config) + case "mem": + cstore = store.NewMemoryStore() + default: + cstore = store.NewMemoryStore() + } + return cstore, err +} + func main() { flag.Parse() config, err := readConfig(*cfg) @@ -234,15 +282,19 @@ func main() { default: log.Fatalln("Unknown provider %s", config.Auth.Provider) } - if err != nil { log.Fatal(err) } + certstore, err := certStore(config.Server.Datastore) + if err != nil { + log.Fatal(err) + } ctx := &appContext{ cookiestore: sessions.NewCookieStore([]byte(config.Server.CookieSecret)), authprovider: authprovider, sshKeySigner: signer, + certstore: certstore, } ctx.cookiestore.Options = &sessions.Options{ MaxAge: 900, @@ -256,6 +308,8 @@ func main() { r.Handle("/auth/login", appHandler{ctx, loginHandler}) r.Handle("/auth/callback", appHandler{ctx, callbackHandler}) r.Handle("/sign", appHandler{ctx, signHandler}) + r.Handle("/revoked", appHandler{ctx, revokedCertsHandler}) + r.Handle("/revoke", appHandler{ctx, revokeCertHandler}) logfile := os.Stderr if config.Server.HTTPLogFile != "" { logfile, err = os.OpenFile(config.Server.HTTPLogFile, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0660) diff --git a/cmd/dbinit/dbinit.go b/cmd/dbinit/dbinit.go new file mode 100644 index 0000000..4431ced --- /dev/null +++ b/cmd/dbinit/dbinit.go @@ -0,0 +1,61 @@ +package main + +import ( + "database/sql" + "flag" + "fmt" + "log" + "strings" + + "github.com/go-sql-driver/mysql" +) + +var ( + host = flag.String("host", "localhost", "host[:port]") + adminUser = flag.String("admin_user", "root", "Admin user") + adminPasswd = flag.String("admin_password", "", "Admin password") + dbUser = flag.String("db_user", "root", "Database user") + dbPasswd = flag.String("db_password", "", "Admin password") +) + +func main() { + flag.Parse() + var createTableStmt = []string{ + `CREATE DATABASE IF NOT EXISTS certs DEFAULT CHARACTER SET = 'utf8' DEFAULT COLLATE 'utf8_general_ci';`, + `USE certs;`, + `CREATE TABLE IF NOT EXISTS issued_certs ( + key_id VARCHAR(255) NOT NULL, + principals VARCHAR(255) NULL, + created_at INT(11) NULL, + expires_at INT(11) NULL, + revoked BOOLEAN DEFAULT NULL, + raw_key TEXT NULL, + PRIMARY KEY (key_id) + );`, + `GRANT ALL PRIVILEGES ON certs.* TO '` + *dbUser + `'@'%' IDENTIFIED BY '` + *dbPasswd + `';`, + } + + if len(strings.Split(*host, ":")) != 2 { + *host = fmt.Sprintf("%s:3306", *host) + } + conn := &mysql.Config{ + User: *adminUser, + Passwd: *adminPasswd, + Net: "tcp", + Addr: *host, + } + db, err := sql.Open("mysql", conn.FormatDSN()) + if err != nil { + log.Fatalf("Error connecting to database: %v", err) + } + defer db.Close() + if err := db.Ping(); err != nil { + log.Fatalf("Unable to connect to database.") + } + for _, stmt := range createTableStmt { + _, err := db.Exec(stmt) + if err != nil { + log.Fatalf("Error running setup: %v", err) + } + } +} -- cgit v1.2.3