diff --git a/token-validator/challenge.go b/token-validator/challenge.go new file mode 100644 index 0000000..99a7255 --- /dev/null +++ b/token-validator/challenge.go @@ -0,0 +1,253 @@ +package main + +import ( + "crypto/hmac" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "log" + "net/http" + "strconv" + "strings" + "time" + + "github.com/julienschmidt/httprouter" + "golang.org/x/crypto/blake2b" +) + +const IPgwDMZ = "172.23.200.1" + +type Challenge struct { + Accessible []func(*Student, *http.Request) error + Check func(*Student, *givenToken, int) error +} + +/* Restrictions */ + +func noAccessRestriction(*Student, *http.Request) error { + return nil +} + +func accessFrom(ip string) func(_ *Student, r *http.Request) error { + return func(_ *Student, r *http.Request) error { + if r.Header.Get("X-Forwarded-By") != ip { + return errors.New("This challenge is not accessible this way.") + } + return nil + } +} + +func notAccessFrom(ip string) func(_ *Student, r *http.Request) error { + return func(_ *Student, r *http.Request) error { + if r.Header.Get("X-Forwarded-By") == ip { + return errors.New("This challenge is not accessible this way.") + } + return nil + } +} + +func maxProxy(nb int) func(_ *Student, r *http.Request) error { + return func(_ *Student, r *http.Request) error { + if len(strings.Split(r.Header.Get("X-Forwarded-For"), ",")) > nb { + return errors.New("This challenge is not accessible this way.") + } + return nil + } +} + +func sslOnly(_ *Student, r *http.Request) error { + if r.Header.Get("X-Forwarded-Proto") != "https" { + return errors.New("This challenge should be performed over TLS.") + } + return nil +} + + +/* Challenges */ + +func challenge42(s *Student, t *givenToken, chid int) error { + pkey := s.GetPKey() + if expectedToken, err := GenerateToken(pkey, chid, []byte("42")); err != nil { + return err + } else if ! hmac.Equal(expectedToken, t.token) { + return errors.New("This is not the expected token.") + } else { + return nil + } +} + +func challengeDNS(s *Student, t *givenToken, chid int) error { + pkey := s.GetPKey() + if expectedToken, err := GenerateToken(pkey, chid, []byte("8dde678132d6c558fc6adaeb9f1d53bf6ec7b876308cf98c48604caa9138523c1ce58b672c87c7e7d9b7248b81804d3940dbf20bf263eeb683244f7c1143712d")); err != nil { + return err + } else if ! hmac.Equal(expectedToken, t.token) { + return errors.New("This is not the expected token.") + } else { + return nil + } +} + +func challengeTime(s *Student, t *givenToken, chid int) error { + pkey := s.GetPKey() + if expectedToken, err := GenerateToken(pkey, chid, []byte(t.Data[0])); err != nil { + return err + } else if ! hmac.Equal(expectedToken, t.token) { + return errors.New("This is not the expected token.") + } else if t, err := strconv.ParseInt(t.Data[0], 10, 64); err != nil { + return err + } else { + var rt time.Time + if t > 3000000000 { + rt = time.Unix(t / 1000000000, t % 1000000000) + } else { + rt = time.Unix(t, 0) + } + if rt.After(time.Now()) { + return errors.New("You seem to live in the future...") + } else if d := rt.Sub(time.Now()); d.Minutes() < -10 { + return errors.New(fmt.Sprintf("Your time is %.0f minutes from us", d.Minutes())) + } + return nil + } +} + +func challengeDisk(s *Student, t *givenToken, chid int) error { + pkey := s.GetPKey() + + n1, err := strconv.Atoi(t.Token[0:2]) + if err != nil { + return err + } + n2, err := strconv.Atoi(t.Token[2:4]) + if err != nil { + return err + } + + sum := make([]byte, hex.DecodedLen(len(t.Token[4:]))) + if _, err := hex.Decode(t.token, []byte(t.Token[4:])); err != nil { + return err + } + + expectedToken := blake2b.Sum512([]byte(pkey[n1:n2])) + + if ! hmac.Equal(expectedToken[:], sum) { + return errors.New("This is not the expected token.") + } else { + return nil + } +} + +var challenges []Challenge + +func init() { + challenges = []Challenge{ + /* Challenge 1 : 42 */ + Challenge{ + Accessible: []func(*Student, *http.Request) error{noAccessRestriction}, + Check: challenge42, + }, + + /* Challenge 2 : 42 from DMZ */ + Challenge{ + Accessible: []func(*Student, *http.Request) error{accessFrom(IPgwDMZ)}, + Check: challenge42, + }, + + /* Challenge 3 : ssl (+ ntp) */ + Challenge{ + Accessible: []func(*Student, *http.Request) error{accessFrom(IPgwDMZ), sslOnly}, + Check: challengeTime, + }, + + /* Challenge 4 : disk */ + Challenge{ + Accessible: []func(*Student, *http.Request) error{noAccessRestriction}, + Check: challengeDisk, + }, + + /* Challenge 5 : DNS TXT */ + Challenge{ + Accessible: []func(*Student, *http.Request) error{accessFrom(IPgwDMZ), sslOnly}, + Check: challengeDNS, + }, + + /* Challenge 6 : time net */ + Challenge{ + Accessible: []func(*Student, *http.Request) error{maxProxy(1)}, + Check: challengeTime, + }, + } + + router.GET("/challenge", apiHandler(getChallengeList)) + router.GET("/challenge/:chid", rawHandler(accessibleChallenge)) + router.POST("/challenge", rawHandler(receiveToken)) +} + +type givenToken struct { + Login string `json:"login"` + Challenge int `json:"challenge"` + Token string `json:"token"` + token []byte + Data []string `json:"data"` +} + +func getChallengeList(_ httprouter.Params, _ []byte) (interface{}, error) { + var ret []int + for i := range challenges { + ret = append(ret, i) + } + return ret, nil +} + +func accessibleChallenge(r *http.Request, ps httprouter.Params, _ []byte) (interface{}, error) { + if chid, err := strconv.Atoi(string(ps.ByName("chid"))); err != nil { + return nil, err + } else if chid == 0 || chid >= len(challenges) { + return nil, errors.New("This challenge doesn't exist") + } else { + for _, a := range challenges[chid - 1].Accessible { + if err := a(nil, r); err != nil { + return nil, err + } + } + return true, nil + } +} + +func receiveToken(r *http.Request, ps httprouter.Params, body []byte) (interface{}, error) { + if _, err := accessibleChallenge(r, ps, body); err != nil { + return nil, err + } + + var gt givenToken + if err := json.Unmarshal(body, >); err != nil { + return nil, err + } + + gt.token = make([]byte, hex.DecodedLen(len(gt.Token))) + if _, err := hex.Decode(gt.token, []byte(gt.Token)); err != nil { + return nil, err + } + + if chid, err := strconv.Atoi(string(ps.ByName("chid"))); err != nil { + return nil, err + } else if chid == 0 || chid > len(challenges) { + return nil, errors.New("This challenge doesn't exist") + } else if std, err := getStudentByLogin(gt.Login); err != nil { + return nil, err + } else { + if err := challenges[chid - 1].Check(&std, >, chid); err != nil { + log.Printf("%s just try ch#%d: %s\n", std.Login, chid, err) + return nil, err + } + + if _, err := std.UnlockNewChallenge(chid, gt.Token); err != nil { + log.Println(err) + return nil, err + } + + log.Printf("%s just unlock ch#%d\n", std.Login, chid) + return "Success", nil + } +} diff --git a/token-validator/handler.go b/token-validator/handler.go index 79b5e49..47d2d26 100644 --- a/token-validator/handler.go +++ b/token-validator/handler.go @@ -38,7 +38,7 @@ func remoteValidatorHandler(f func(http.ResponseWriter, *http.Request, httproute } } -func apiHandler(f DispatchFunction) func(http.ResponseWriter, *http.Request, httprouter.Params) { +func rawHandler(f func(*http.Request, httprouter.Params, []byte) (interface{}, error)) func(http.ResponseWriter, *http.Request, httprouter.Params) { return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { if addr := r.Header.Get("X-Forwarded-For"); addr != "" { r.RemoteAddr = addr @@ -69,7 +69,7 @@ func apiHandler(f DispatchFunction) func(http.ResponseWriter, *http.Request, htt } } - ret, err = f(ps, body) + ret, err = f(r, ps, body) // Format response resStatus := http.StatusOK @@ -99,6 +99,10 @@ func apiHandler(f DispatchFunction) func(http.ResponseWriter, *http.Request, htt } } +func apiHandler(f DispatchFunction) func(http.ResponseWriter, *http.Request, httprouter.Params) { + return rawHandler(func (_ *http.Request, ps httprouter.Params, b []byte) (interface{}, error) { return f(ps, b) }) +} + func studentHandler(f func(Student, []byte) (interface{}, error)) func(httprouter.Params, []byte) (interface{}, error) { return func(ps httprouter.Params, body []byte) (interface{}, error) { if sid, err := strconv.Atoi(string(ps.ByName("sid"))); err != nil { diff --git a/token-validator/students.go b/token-validator/students.go index e49b1d3..a0dda79 100644 --- a/token-validator/students.go +++ b/token-validator/students.go @@ -1,6 +1,8 @@ package main import ( + "crypto/hmac" + "crypto/sha512" "encoding/json" "strings" "time" @@ -10,19 +12,21 @@ import ( func init() { router.GET("/api/students/", apiHandler( - func(httprouter.Params,[]byte) (interface{}, error) { - return getStudents() })) + func(httprouter.Params, []byte) (interface{}, error) { + return getStudents() + })) router.POST("/api/students/", remoteValidatorHandler(apiHandler(createStudent))) router.GET("/api/students/:sid/", apiHandler(studentHandler( func(std Student, _ []byte) (interface{}, error) { - return std, nil }))) + return std, nil + }))) router.PUT("/api/students/:sid/", remoteValidatorHandler(apiHandler(studentHandler(updateStudent)))) router.DELETE("/api/students/:sid/", remoteValidatorHandler(apiHandler(studentHandler( func(std Student, _ []byte) (interface{}, error) { - return std.Delete() })))) + return std.Delete() + })))) } - type Student struct { Id int64 `json:"id"` Login string `json:"login"` @@ -70,6 +74,10 @@ func NewStudent(login string) (Student, error) { } } +func (s Student) GetPKey() []byte { + return hmac.New(sha512.New512_224, []byte(sharedSecret)).Sum([]byte(s.Login)) +} + func (s Student) Update() (int64, error) { if res, err := DBExec("UPDATE students SET login = ?, time = ? WHERE id_student = ?", s.Login, s.Time, s.Id); err != nil { return 0, err @@ -100,7 +108,6 @@ func ClearStudents() (int64, error) { } } - func createStudent(_ httprouter.Params, body []byte) (interface{}, error) { var std Student if err := json.Unmarshal(body, &std); err != nil { @@ -120,3 +127,20 @@ func updateStudent(current Student, body []byte) (interface{}, error) { current.Time = new.Time return current.Update() } + +type UnlockedChallenge struct { + Id int64 `json:"id"` + IdStudent int64 `json:"id_student"` + Challenge int `json:"challenge"` + Time time.Time `json:"time"` +} + +func (s Student) UnlockNewChallenge(challenge int, value string) (UnlockedChallenge, error) { + if res, err := DBExec("INSERT INTO student_challenges (id_student, challenge, time, value) VALUES (?, ?, ?, ?)", s.Id, challenge, time.Now(), value); err != nil { + return UnlockedChallenge{}, err + } else if utid, err := res.LastInsertId(); err != nil { + return UnlockedChallenge{}, err + } else { + return UnlockedChallenge{utid, s.Id, challenge, time.Now()}, err + } +} diff --git a/token-validator/token.go b/token-validator/token.go new file mode 100644 index 0000000..d00320e --- /dev/null +++ b/token-validator/token.go @@ -0,0 +1,24 @@ +package main + +import ( + "fmt" + + "golang.org/x/crypto/blake2b" +) + +func GenerateToken(pkey []byte, id int, a... []byte) ([]byte, error) { + h, err := blake2b.New(blake2b.Size, nil) + if err != nil { + return nil, err + } + + h.Write(pkey) + h.Write([]byte(fmt.Sprintf(":%d", id))) + + for _, v := range a { + h.Write([]byte(":")) + h.Write(v) + } + + return h.Sum(nil), nil +}