package api import ( "crypto/rand" "crypto/sha1" "crypto/x509" "crypto/x509/pkix" "encoding/base32" "encoding/base64" "errors" "fmt" "io/ioutil" "log" "math" "math/big" "net/http" "os" "path" "strconv" "strings" "time" "srs.epita.fr/fic-server/admin/pki" "srs.epita.fr/fic-server/libfic" "github.com/gin-gonic/gin" ) var TeamsDir string func declareCertificateRoutes(router *gin.RouterGroup) { router.GET("/htpasswd", func(c *gin.Context) { ret, err := genHtpasswd(true) if err != nil { c.AbortWithError(http.StatusInternalServerError, err) return } c.String(http.StatusOK, ret) }) router.POST("/htpasswd", func(c *gin.Context) { if htpasswd, err := genHtpasswd(true); err != nil { log.Println("Unable to generate htpasswd:", err) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) return } else if err := ioutil.WriteFile(path.Join(pki.PKIDir, "shared", "ficpasswd"), []byte(htpasswd), 0644); err != nil { log.Println("Unable to write htpasswd:", err) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) return } c.AbortWithStatus(http.StatusOK) }) router.DELETE("/htpasswd", func(c *gin.Context) { if err := os.Remove(path.Join(pki.PKIDir, "shared", "ficpasswd")); err != nil { log.Println("Unable to remove htpasswd:", err) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) return } c.AbortWithStatus(http.StatusOK) }) router.GET("/htpasswd.apr1", func(c *gin.Context) { ret, err := genHtpasswd(false) if err != nil { c.AbortWithError(http.StatusInternalServerError, err) return } c.String(http.StatusOK, ret) }) router.GET("/ca", infoCA) router.GET("/ca.pem", getCAPEM) router.POST("/ca/new", func(c *gin.Context) { var upki PKISettings err := c.ShouldBindJSON(&upki) if err != nil { c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) return } if err := pki.GenerateCA(upki.NotBefore, upki.NotAfter); err != nil { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) return } c.JSON(http.StatusCreated, true) }) router.GET("/certs", getCertificates) router.POST("/certs", generateClientCert) router.DELETE("/certs", func(c *gin.Context) { v, err := fic.ClearCertificates() if err != nil { log.Println("Unable to ClearCertificates:", err.Error()) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) return } c.JSON(http.StatusOK, v) }) apiCertificatesRoutes := router.Group("/certs/:certid") apiCertificatesRoutes.Use(CertificateHandler) apiCertificatesRoutes.HEAD("", getTeamP12File) apiCertificatesRoutes.GET("", getTeamP12File) apiCertificatesRoutes.PUT("", updateCertificateAssociation) apiCertificatesRoutes.DELETE("", func(c *gin.Context) { cert := c.MustGet("cert").(*fic.Certificate) v, err := cert.Revoke() if err != nil { log.Println("Unable to Revoke:", err.Error()) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) return } c.JSON(http.StatusOK, v) }) } func declareTeamCertificateRoutes(router *gin.RouterGroup) { router.GET("/certificates", func(c *gin.Context) { team := c.MustGet("team").(*fic.Team) if serials, err := pki.GetTeamSerials(TeamsDir, team.Id); err != nil { log.Println("Unable to GetTeamSerials:", err.Error()) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) return } else { var certs []CertExported for _, serial := range serials { if cert, err := fic.GetCertificate(serial); err == nil { certs = append(certs, CertExported{fmt.Sprintf("%0[2]*[1]X", cert.Id, int(math.Ceil(math.Log2(float64(cert.Id))/8)*2)), cert.Creation, cert.Password, &team.Id, cert.Revoked}) } else { log.Println("Unable to get back certificate, whereas an association exists on disk: ", err) } } c.JSON(http.StatusOK, certs) } }) router.GET("/associations", func(c *gin.Context) { team := c.MustGet("team").(*fic.Team) assocs, err := pki.GetTeamAssociations(TeamsDir, team.Id) if err != nil { log.Println("Unable to GetTeamAssociations:", err.Error()) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) return } c.JSON(http.StatusOK, assocs) }) apiTeamAssociationsRoutes := router.Group("/associations/:assoc") apiTeamAssociationsRoutes.POST("", func(c *gin.Context) { team := c.MustGet("team").(*fic.Team) if err := os.Symlink(fmt.Sprintf("%d", team.Id), path.Join(TeamsDir, c.Params.ByName("assoc"))); err != nil { log.Println("Unable to create association symlink:", err.Error()) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to create association symlink: %s", err.Error())}) return } c.JSON(http.StatusOK, c.Params.ByName("assoc")) }) apiTeamAssociationsRoutes.DELETE("", func(c *gin.Context) { err := pki.DeleteTeamAssociation(TeamsDir, c.Params.ByName("assoc")) if err != nil { log.Printf("Unable to DeleteTeamAssociation(%s): %s", c.Params.ByName("assoc"), err.Error()) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to delete association symlink: %s", err.Error())}) return } c.JSON(http.StatusOK, nil) }) } func CertificateHandler(c *gin.Context) { var certid uint64 var err error cid := strings.TrimSuffix(string(c.Params.ByName("certid")), ".p12") if certid, err = strconv.ParseUint(cid, 10, 64); err != nil { if certid, err = strconv.ParseUint(cid, 16, 64); err != nil { c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid certficate identifier"}) return } } cert, err := fic.GetCertificate(certid) if err != nil { c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Certificate not found"}) return } c.Set("cert", cert) c.Next() } func genHtpasswd(ssha bool) (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 { // Ignore invalid/incorrect/non-existant certificates continue } if cert.Revoked != nil { continue } salt := make([]byte, 5) if _, err = rand.Read(salt); err != nil { return } if ssha { hash := sha1.New() hash.Write([]byte(cert.Password)) hash.Write([]byte(salt)) passwdline := fmt.Sprintf(":{SSHA}%s\n", base64.StdEncoding.EncodeToString(append(hash.Sum(nil), salt...))) ret += strings.ToLower(team.Name) + passwdline ret += fmt.Sprintf("%0[2]*[1]x", cert.Id, int(math.Ceil(math.Log2(float64(cert.Id))/8)*2)) + passwdline ret += fmt.Sprintf("%0[2]*[1]X", cert.Id, int(math.Ceil(math.Log2(float64(cert.Id))/8)*2)) + passwdline teamAssociations, _ := pki.GetTeamAssociations(TeamsDir, team.Id) log.Println(path.Join(TeamsDir, fmt.Sprintf("%d", team.Id)), teamAssociations) for _, ta := range teamAssociations { ret += strings.Replace(ta, ":", "", -1) + passwdline } } else { salt32 := base32.StdEncoding.EncodeToString(salt) ret += fmt.Sprintf( "%s:$apr1$%s$%s\n", strings.ToLower(team.Name), salt32, fic.Apr1Md5(cert.Password, salt32), ) } } } return } type PKISettings struct { Version int `json:"version"` SerialNumber *big.Int `json:"serialnumber"` Issuer pkix.Name `json:"issuer"` Subject pkix.Name `json:"subject"` NotBefore time.Time `json:"notbefore"` NotAfter time.Time `json:"notafter"` SignatureAlgorithm x509.SignatureAlgorithm `json:"signatureAlgorithm,"` PublicKeyAlgorithm x509.PublicKeyAlgorithm `json:"publicKeyAlgorithm"` } func infoCA(c *gin.Context) { _, cacert, err := pki.LoadCA() if err != nil { c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "CA not found"}) return } c.JSON(http.StatusOK, PKISettings{ Version: cacert.Version, SerialNumber: cacert.SerialNumber, Issuer: cacert.Issuer, Subject: cacert.Subject, NotBefore: cacert.NotBefore, NotAfter: cacert.NotAfter, SignatureAlgorithm: cacert.SignatureAlgorithm, PublicKeyAlgorithm: cacert.PublicKeyAlgorithm, }) } func getCAPEM(c *gin.Context) { if _, err := os.Stat(pki.CACertPath()); os.IsNotExist(err) { c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Unable to locate the CA root certificate. Have you generated it?"}) return } else if fd, err := os.Open(pki.CACertPath()); err != nil { log.Println("Unable to open CA root certificate:", err) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) return } else { defer fd.Close() cnt, err := ioutil.ReadAll(fd) if err != nil { log.Println("Unable to read CA root certificate:", err) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) return } c.String(http.StatusOK, string(cnt)) } } func getTeamP12File(c *gin.Context) { cert := c.MustGet("cert").(*fic.Certificate) // Create p12 if necessary if _, err := os.Stat(pki.ClientP12Path(cert.Id)); os.IsNotExist(err) { if err := pki.WriteP12(cert.Id, cert.Password); err != nil { log.Println("Unable to WriteP12:", err.Error()) c.AbortWithError(http.StatusInternalServerError, err) return } } if _, err := os.Stat(pki.ClientP12Path(cert.Id)); os.IsNotExist(err) { log.Println("Unable to compute ClientP12Path:", err.Error()) c.AbortWithError(http.StatusInternalServerError, errors.New("Unable to locate the p12. Have you generated it?")) return } else if fd, err := os.Open(pki.ClientP12Path(cert.Id)); err != nil { log.Println("Unable to open ClientP12Path:", err.Error()) c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("Unable to open the p12: %w", err)) return } else { defer fd.Close() data, err := ioutil.ReadAll(fd) if err != nil { log.Println("Unable to open ClientP12Path:", err.Error()) c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("Unable to open the p12: %w", err)) return } c.Data(http.StatusOK, "application/x-pkcs12", data) } } func generateClientCert(c *gin.Context) { // First, generate a new, unique, serial var serial_gen [8]byte if _, err := rand.Read(serial_gen[:]); err != nil { log.Println("Unable to read enough entropy to generate client certificate:", err) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to read enough entropy"}) return } for fic.ExistingCertSerial(serial_gen) { if _, err := rand.Read(serial_gen[:]); err != nil { log.Println("Unable to read enough entropy to generate client certificate:", err) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to read enough entropy"}) return } } var serial_b big.Int serial_b.SetBytes(serial_gen[:]) serial := serial_b.Uint64() // Let's pick a random password password, err := fic.GeneratePassword() if err != nil { log.Println("Unable to generate password:", err) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to generate password: " + err.Error()}) return } // Ok, now load CA capriv, cacert, err := pki.LoadCA() if err != nil { log.Println("Unable to load the CA:", err) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to load the CA"}) return } // Generate our privkey if err := pki.GenerateClient(serial, cacert.NotBefore, cacert.NotAfter, &cacert, &capriv); err != nil { log.Println("Unable to generate private key:", err) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to generate private key: " + err.Error()}) return } // Save in DB cert, err := fic.RegisterCertificate(serial, password) if err != nil { log.Println("Unable to register certificate:", err) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to register certificate."}) return } c.JSON(http.StatusOK, CertExported{fmt.Sprintf("%0[2]*[1]X", cert.Id, int(math.Ceil(math.Log2(float64(cert.Id))/8)*2)), cert.Creation, cert.Password, nil, cert.Revoked}) } type CertExported struct { Id string `json:"id"` Creation time.Time `json:"creation"` Password string `json:"password,omitempty"` IdTeam *int64 `json:"id_team"` Revoked *time.Time `json:"revoked"` } func getCertificates(c *gin.Context) { certificates, err := fic.GetCertificates() if err != nil { log.Println("Unable to retrieve certificates list:", err) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during certificates retrieval."}) return } ret := make([]CertExported, 0) for _, cert := range certificates { dstLinkPath := path.Join(TeamsDir, pki.GetCertificateAssociation(cert.Id)) var idTeam *int64 = nil if lnk, err := os.Readlink(dstLinkPath); err == nil { if tid, err := strconv.ParseInt(lnk, 10, 64); err == nil { idTeam = &tid } } ret = append(ret, CertExported{fmt.Sprintf("%0[2]*[1]X", cert.Id, int(math.Ceil(math.Log2(float64(cert.Id))/8)*2)), cert.Creation, "", idTeam, cert.Revoked}) } c.JSON(http.StatusOK, ret) } type CertUploaded struct { Team *int64 `json:"id_team"` } func updateCertificateAssociation(c *gin.Context) { cert := c.MustGet("cert").(*fic.Certificate) var uc CertUploaded err := c.ShouldBindJSON(&uc) if err != nil { c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) return } dstLinkPath := path.Join(TeamsDir, pki.GetCertificateAssociation(cert.Id)) if uc.Team != nil { srcLinkPath := fmt.Sprintf("%d", *uc.Team) if err := os.Symlink(srcLinkPath, dstLinkPath); err != nil { log.Println("Unable to create certificate symlink:", err.Error()) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to create certificate symlink: %s", err.Error())}) return } // Mark team as active to ensure it'll be generated if ut, err := fic.GetTeam(*uc.Team); err != nil { log.Println("Unable to GetTeam:", err.Error()) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during team retrieval."}) return } else if !ut.Active { ut.Active = true _, err := ut.Update() if err != nil { log.Println("Unable to UpdateTeam after updateCertificateAssociation:", err.Error()) } } } else { os.Remove(dstLinkPath) } c.JSON(http.StatusOK, cert) }