├── .gitignore ├── LICENSE ├── README.md ├── api.go ├── dal.go ├── gate.go └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | database.db 2 | yubikey-server 3 | 4 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 5 | *.o 6 | *.a 7 | *.so 8 | 9 | # Folders 10 | _obj 11 | _test 12 | 13 | # Architecture specific extensions/prefixes 14 | *.[568vq] 15 | [568vq].out 16 | 17 | *.cgo1.go 18 | *.cgo2.c 19 | _cgo_defun.c 20 | _cgo_gotypes.go 21 | _cgo_export.* 22 | 23 | _testmain.go 24 | 25 | *.exe 26 | *.test 27 | *.prof 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Niels Freier 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/8a35c3cfc6e14db99369bcbbb887220d)](https://www.codacy.com/app/stumpyfr/yubikey-server?utm_source=github.com&utm_medium=referral&utm_content=stumpyfr/yubikey-server&utm_campaign=badger) 4 | 5 | Go implementation of yubikey server to be able to run your own server on network who can't have access to the official servers. 6 | 7 | Store all information inside a sqlite database, need to create other connectors for different backend 8 | 9 | I followed the [yubikey protocol in version 2.0](https://developers.yubico.com/yubikey-val/Validation_Protocol_V2.0.html) to implement this server 10 | 11 | # Usage 12 | 13 | // to build the server 14 | $go build 15 | 16 | // will add a new application and display the id and key 17 | $./yubikey-server -app "NameOfYourApp" 18 | 19 | // will add a new key in the system 20 | $./yubikey-server -name "YourName" -pub "publicKey" -secret "AESSecret" 21 | 22 | // will revoke/delete a key 23 | $./yubikey-server -delete "YourName" 24 | 25 | // will start the server on the default port 3000 26 | $./yubikey-server -s 27 | 28 | # How to query the server 29 | Get http call: 30 | 31 | http://:/wsapi/2.0/verify?otp=&id=&nonce=test42 32 | 33 | Will return: 34 | 35 | nonce=test42 36 | opt=vvcfvelvtdfvtvviihlihlvgnbhnffbgjhdevrfckbfi 37 | status=OK 38 | t=2015-01-03T02:11:05+04:00 39 | h=Vx8RgAAtjypv504iSPbT5nYCt3U= 40 | 41 | otp is the one time password generated by your key and app id the the id returned by the server when you created a new application with the "-app " argument 42 | -------------------------------------------------------------------------------- /api.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha1" 6 | "encoding/base64" 7 | "errors" 8 | "github.com/gorilla/mux" 9 | "log" 10 | "net/http" 11 | "time" 12 | "sort" 13 | ) 14 | 15 | const ( 16 | pubLen int = 12 17 | ) 18 | 19 | func checkOTP(w http.ResponseWriter, r *http.Request, dal *Dal) { 20 | if r.URL.Query()["otp"] == nil || r.URL.Query()["nonce"] == nil || r.URL.Query()["id"] == nil { 21 | reply(w, "", "", "", MISSING_PARAMETER, "", dal) 22 | return 23 | } 24 | otp := r.URL.Query()["otp"][0] 25 | nonce := r.URL.Query()["nonce"][0] 26 | id := r.URL.Query()["id"][0] 27 | name := "" 28 | 29 | if len(otp) < pubLen { 30 | reply(w, otp, name, nonce, BAD_OTP, id, dal) 31 | return 32 | } 33 | pub := otp[:pubLen] 34 | 35 | k, err := dal.GetKey(pub) 36 | if err != nil { 37 | reply(w, otp, name, nonce, BAD_OTP, id, dal) 38 | return 39 | } else { 40 | k, err = Gate(k, otp) 41 | if err != nil { 42 | reply(w, otp, name, nonce, err.Error(), id, dal) 43 | return 44 | } else { 45 | err = dal.UpdateKey(k) 46 | if err != nil { 47 | log.Println("fail to update key counter/session") 48 | return 49 | } 50 | 51 | name := k.Name 52 | reply(w, otp, name, nonce, OK, id, dal) 53 | return 54 | } 55 | } 56 | } 57 | 58 | func Sign(values []string, key []byte) []byte { 59 | payload := "" 60 | // Alphabetically sort the set of key/value pairs by key order. 61 | // ref: https://developers.yubico.com/yubikey-val/Validation_Protocol_V2.0.html 62 | sort.Strings(values) 63 | for _, v := range values { 64 | payload += v + "&" 65 | } 66 | payload = payload[:len(payload)-1] 67 | 68 | mac := hmac.New(sha1.New, key) 69 | mac.Write([]byte(payload)) 70 | return mac.Sum(nil) 71 | } 72 | 73 | func loadKey(id string, dal *Dal) ([]byte, error) { 74 | i, err := dal.GetApp(id) 75 | if err != nil { 76 | return []byte{}, errors.New(NO_SUCH_CLIENT) 77 | } 78 | 79 | return i, nil 80 | } 81 | 82 | func reply(w http.ResponseWriter, otp, name, nonce, status, id string, dal *Dal) { 83 | values := []string{} 84 | key := []byte{} 85 | err := errors.New("") 86 | 87 | values = append(values, "nonce="+nonce) 88 | values = append(values, "otp="+otp) 89 | if status != MISSING_PARAMETER { 90 | key, err = loadKey(id, dal) 91 | if err == nil { 92 | values = append(values, "name="+name) 93 | values = append(values, "status="+status) 94 | } else { 95 | values = append(values, "status="+err.Error()) 96 | } 97 | } else { 98 | values = append(values, "status="+status) 99 | } 100 | values = append(values, "t="+time.Now().Format(time.RFC3339)) 101 | if status != MISSING_PARAMETER { 102 | values = append(values, "h="+base64.StdEncoding.EncodeToString(Sign(values, key))) 103 | } 104 | 105 | ret := "" 106 | for _, v := range values { 107 | ret += v + "\n" 108 | } 109 | 110 | w.Write([]byte(ret)) 111 | } 112 | 113 | func runAPI(dal *Dal, host, port string) { 114 | r := mux.NewRouter() 115 | 116 | r.HandleFunc("/wsapi/2.0/verify", func(w http.ResponseWriter, r *http.Request) { 117 | checkOTP(w, r, dal) 118 | }).Methods("GET") 119 | 120 | http.Handle("/", r) 121 | log.Printf("Listening on: %s:%s...", host, port) 122 | http.ListenAndServe(host+":"+port, nil) 123 | } 124 | -------------------------------------------------------------------------------- /dal.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "encoding/base64" 6 | "errors" 7 | _ "github.com/mattn/go-sqlite3" 8 | "time" 9 | ) 10 | 11 | type Dal struct { 12 | db *sql.DB 13 | } 14 | 15 | type Key struct { 16 | Id int 17 | Name string 18 | Created *time.Time 19 | Used *time.Time 20 | Counter int 21 | Session int 22 | Public string 23 | Secret string 24 | } 25 | 26 | type App struct { 27 | Id int 28 | Name string 29 | Key []byte 30 | } 31 | 32 | func newDAL(dbfile string) (*Dal, error) { 33 | d, err := sql.Open("sqlite3", dbfile) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | ret := Dal{db: d} 39 | ret.init() 40 | 41 | return &ret, nil 42 | } 43 | 44 | func (d *Dal) CreateApp(app *App) (*App, error) { 45 | // Truncate the key to the length we expect 46 | app.Key = Sign([]string{app.Name}, app.Key) 47 | 48 | stmt, err := d.db.Prepare(`insert into apps(name, key, created) values(?, ?, ?)`) 49 | if err != nil { 50 | return nil, err 51 | } 52 | defer stmt.Close() 53 | _, err = stmt.Exec(app.Name, base64.StdEncoding.EncodeToString(app.Key), time.Now()) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | stmt2, err := d.db.Prepare("select MAX(id) from apps LIMIT 1") 59 | if err != nil { 60 | return nil, err 61 | } 62 | defer stmt2.Close() 63 | err = stmt2.QueryRow().Scan(&app.Id) 64 | if err != nil { 65 | return nil, err 66 | } 67 | 68 | return app, nil 69 | } 70 | 71 | func (d *Dal) CreateKey(key *Key) error { 72 | if key.Name == "" { 73 | return errors.New("name need to be indicated") 74 | } else if key.Public == "" { 75 | return errors.New("pub need to be indicated") 76 | } else if key.Secret == "" { 77 | return errors.New("secret need to be indicated") 78 | } else { 79 | k, _ := d.GetKey(key.Public) 80 | if k != nil { 81 | return errors.New("public key: " + key.Public + " already exists") 82 | } else { 83 | stmt, err := d.db.Prepare(`insert into keys(name, created, counter, session, public, secret) values(?, ?, ?, ?, ?, ?)`) 84 | if err != nil { 85 | return err 86 | } 87 | defer stmt.Close() 88 | _, err = stmt.Exec(key.Name, time.Now(), 0, 0, key.Public, key.Secret) 89 | if err != nil { 90 | return err 91 | } 92 | return nil 93 | } 94 | } 95 | return nil 96 | } 97 | 98 | func (d *Dal) DeleteKey(key *Key) error { 99 | if key.Name == "" { 100 | return errors.New("name need to be indicated") 101 | } else { 102 | 103 | stmt, err := d.db.Prepare(`delete from keys where name=?`) 104 | if err != nil { 105 | return err 106 | } 107 | defer stmt.Close() 108 | _, err = stmt.Exec(key.Name) 109 | if err != nil { 110 | return err 111 | } 112 | return nil 113 | 114 | } 115 | return nil 116 | } 117 | 118 | func (d *Dal) UpdateKey(key *Key) error { 119 | stmt, err := d.db.Prepare("update keys set counter = ?, session = ?, used = ?where public = ?") 120 | if err != nil { 121 | return err 122 | } 123 | defer stmt.Close() 124 | _, err = stmt.Exec(key.Counter, key.Session, time.Now(), key.Public) 125 | if err != nil { 126 | return err 127 | } 128 | 129 | return nil 130 | } 131 | 132 | func (d *Dal) GetApp(id string) ([]byte, error) { 133 | stmt, err := d.db.Prepare("select key from apps where id = ?") 134 | if err != nil { 135 | return nil, err 136 | } 137 | defer stmt.Close() 138 | base64key := "" 139 | err = stmt.QueryRow(id).Scan(&base64key) 140 | if err != nil { 141 | return nil, err 142 | } 143 | key, err := base64.StdEncoding.DecodeString(base64key) 144 | if err != nil { 145 | return nil, err 146 | } 147 | return key, nil 148 | } 149 | 150 | func (d *Dal) GetKey(pub string) (*Key, error) { 151 | stmt, err := d.db.Prepare("select name, created, used, counter, session, public, secret from keys where public = ?") 152 | if err != nil { 153 | return nil, err 154 | } 155 | defer stmt.Close() 156 | user := Key{} 157 | err = stmt.QueryRow(pub).Scan(&user.Name, &user.Created, &user.Used, &user.Counter, &user.Session, &user.Public, &user.Secret) 158 | if err != nil { 159 | return nil, err 160 | } 161 | return &user, nil 162 | } 163 | 164 | func (d *Dal) init() { 165 | sqlStmt := ` 166 | create table keys ( 167 | id integer not null primary key AUTOINCREMENT, 168 | name text, 169 | created datetime, 170 | used datetime, 171 | counter int, 172 | session int, 173 | public text, 174 | secret text); 175 | ` 176 | d.db.Exec(sqlStmt) 177 | 178 | sqlStmt = ` 179 | create table apps ( 180 | id integer not null primary key AUTOINCREMENT, 181 | name text, 182 | created datetime, 183 | key text 184 | )` 185 | d.db.Exec(sqlStmt) 186 | return 187 | } 188 | 189 | func (d *Dal) Close() { 190 | d.db.Close() 191 | } 192 | -------------------------------------------------------------------------------- /gate.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/hex" 5 | "errors" 6 | "github.com/conformal/yubikey" 7 | ) 8 | 9 | const ( 10 | OK = "OK" 11 | REPLAYED_OTP = "REPLAYED_OTP" 12 | MISSING_PARAMETER = "MISSING_PARAMETER" 13 | BAD_OTP = "BAD_OTP" 14 | BAD_SIGNATURE = "BAD_SIGNATURE" 15 | NO_SUCH_CLIENT = "NO_SUCH_CLIENT" 16 | ) 17 | 18 | func Gate(key *Key, otp string) (*Key, error) { 19 | priv, err := getSecretKey(key.Secret) 20 | if err != nil { 21 | return nil, err 22 | } 23 | token, err := getToken(otp, priv) 24 | if err != nil { 25 | return nil, errors.New(BAD_OTP) 26 | } 27 | 28 | if token.Ctr < uint16(key.Counter) { 29 | return nil, errors.New(REPLAYED_OTP) 30 | } else if token.Ctr == uint16(key.Counter) && token.Use <= uint8(key.Session) { 31 | return nil, errors.New(REPLAYED_OTP) 32 | } else { 33 | key.Counter = int(token.Ctr) 34 | key.Session = int(token.Use) 35 | } 36 | 37 | return key, nil 38 | } 39 | 40 | func getSecretKey(key string) (*yubikey.Key, error) { 41 | b, err := hex.DecodeString(key) 42 | if err != nil { 43 | return nil, err 44 | } 45 | priv := yubikey.NewKey(b) 46 | 47 | return &priv, nil 48 | } 49 | 50 | func getToken(otpString string, priv *yubikey.Key) (*yubikey.Token, error) { 51 | _, otp, err := yubikey.ParseOTPString(otpString) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | t, err := otp.Parse(*priv) 57 | if err != nil { 58 | return nil, err 59 | } 60 | return t, nil 61 | } 62 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/base64" 6 | "flag" 7 | "fmt" 8 | ) 9 | 10 | func main() { 11 | serverMode := flag.Bool("s", false, "server mode") 12 | name := flag.String("name", "", "name") 13 | delete := flag.String("delete", "", "key to delete") 14 | pub := flag.String("pub", "", "public identity") 15 | secret := flag.String("secret", "", "secret key") 16 | app := flag.String("app", "", "application name") 17 | port := flag.String("p", "4242", "server port") 18 | host := flag.String("host", "127.0.0.1", "server addr") 19 | db := flag.String("db", "database.db", "database file") 20 | flag.Parse() 21 | 22 | dal, err := newDAL(*db) 23 | if err != nil { 24 | fmt.Println(err) 25 | } 26 | 27 | if *serverMode { 28 | runAPI(dal, *host, *port) 29 | } else { 30 | if *app != "" { 31 | randomkey := make([]byte, 256) 32 | _, err := rand.Read(randomkey) 33 | if err != nil { 34 | fmt.Println("error getting random data:", err) 35 | } else { 36 | app, err := dal.CreateApp(&App{Name: *app, Key: randomkey}) 37 | if err != nil { 38 | fmt.Println(err) 39 | } else { 40 | fmt.Println("app created, id:", app.Id, "key:", base64.StdEncoding.EncodeToString(app.Key)) 41 | } 42 | } 43 | } else { 44 | if *delete != "" { 45 | 46 | err := dal.DeleteKey(&Key{Name: *delete}) 47 | if err != nil { 48 | fmt.Println(err) 49 | } else { 50 | fmt.Println("key deleted: OK") 51 | } 52 | 53 | } else { 54 | 55 | err := dal.CreateKey(&Key{Name: *name, Public: *pub, Secret: *secret}) 56 | if err != nil { 57 | fmt.Println(err) 58 | } else { 59 | fmt.Println("creation of the key: OK") 60 | } 61 | } 62 | } 63 | } 64 | } 65 | --------------------------------------------------------------------------------