From 2623d9dd614ec09bdc51fd82bf0d90a4fe14aa89 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Thu, 17 Jan 2019 10:51:44 +0100 Subject: [PATCH] admin: new route to generate htpasswd corresponding to certificate in use by team --- admin/api/certificate.go | 49 ++++++++++++++++++++++++ libfic/utils.go | 80 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+) diff --git a/admin/api/certificate.go b/admin/api/certificate.go index a8fe16c5..3ab0d622 100644 --- a/admin/api/certificate.go +++ b/admin/api/certificate.go @@ -2,6 +2,7 @@ package api import ( "crypto/rand" + "encoding/base32" "encoding/json" "errors" "fmt" @@ -12,6 +13,7 @@ import ( "path" "time" "strconv" + "strings" "srs.epita.fr/fic-server/admin/pki" "srs.epita.fr/fic-server/libfic" @@ -22,6 +24,10 @@ import ( var TeamsDir string func init() { + router.GET("/api/htpasswd", apiHandler( + func(httprouter.Params, []byte) (interface{}, error) { + return genHtpasswd() + })) router.GET("/api/ca/", apiHandler(infoCA)) router.GET("/api/ca.pem", apiHandler(getCAPEM)) router.POST("/api/ca/new", apiHandler( @@ -57,6 +63,49 @@ func init() { func(cert fic.Certificate, _ []byte) (interface{}, error) { return cert.Revoke() }))) } +func genHtpasswd() (ret string, err error) { + var teams []fic.Team + teams, err = fic.GetTeams() + if err != nil { + return + } + + for _, team := range teams { + var serials []uint64 + serials, err = pki.GetTeamSerials(TeamsDir, team.Id) + if err != nil { + return + } + + if len(serials) == 0 { + // Don't include teams that don't have associated certificates + continue + } + + for _, serial := range serials { + var cert fic.Certificate + cert, err = fic.GetCertificate(serial) + if err != nil { + return + } + + if cert.Revoked != nil { + continue + } + + b := make([]byte, 5) + if _, err = rand.Read(b); err != nil { + return + } + salt := base32.StdEncoding.EncodeToString(b) + + ret += fmt.Sprintf("%s:$apr1$%s$%s\n", strings.ToLower(team.Name), salt, fic.Apr1Md5(cert.Password, salt)) + } + } + + return +} + func infoCA(_ httprouter.Params, _ []byte) (interface{}, error) { _, cacert, err := pki.LoadCA() if err != nil { diff --git a/libfic/utils.go b/libfic/utils.go index 17eedeff..d473b95a 100644 --- a/libfic/utils.go +++ b/libfic/utils.go @@ -1,6 +1,8 @@ package fic import ( + "bytes" + "crypto/md5" "regexp" "strings" ) @@ -10,3 +12,81 @@ func ToURLid(str string) string { re := regexp.MustCompile("[^a-zA-Z0-9]+") return strings.TrimSuffix(re.ReplaceAllLiteralString(str, "-"), "-") } + +// Apr1Md5 computes a usable hash for basic auth in Apache and nginx. +// Function copied from https://github.com/jimstudt/http-authentication/blob/master/basic/md5.go +// as it was not exported in package and comes with other unwanted functions. +// Original source code under MIT. +func Apr1Md5(password string, salt string) string { + initBin := md5.Sum([]byte(password + salt + password)) + + initText := bytes.NewBufferString(password + "$apr1$" + salt) + + for i := len(password); i > 0; i -= 16 { + lim := i + if lim > 16 { + lim = 16 + } + initText.Write(initBin[0:lim]) + } + + for i := len(password); i > 0; i >>= 1 { + if (i & 1) == 1 { + initText.WriteByte(byte(0)) + } else { + initText.WriteByte(password[0]) + } + } + + bin := md5.Sum(initText.Bytes()) + + n := bytes.NewBuffer([]byte{}) + + for i := 0; i < 1000; i++ { + n.Reset() + + if (i & 1) == 1 { + n.WriteString(password) + } else { + n.Write(bin[:]) + } + + if i%3 != 0 { + n.WriteString(salt) + } + + if i%7 != 0 { + n.WriteString(password) + } + + if (i & 1) == 1 { + n.Write(bin[:]) + } else { + n.WriteString(password) + } + + bin = md5.Sum(n.Bytes()) + } + + result := bytes.NewBuffer([]byte{}) + + fill := func(a byte, b byte, c byte) { + v := (uint(a) << 16) + (uint(b) << 8) + uint(c) + + for i := 0; i < 4; i++ { + result.WriteByte("./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"[v&0x3f]) + v >>= 6 + } + } + + fill(bin[0], bin[6], bin[12]) + fill(bin[1], bin[7], bin[13]) + fill(bin[2], bin[8], bin[14]) + fill(bin[3], bin[9], bin[15]) + fill(bin[4], bin[10], bin[5]) + fill(0, 0, bin[11]) + + resultString := string(result.Bytes()[0:22]) + + return resultString +}