480 lines
15 KiB
Go
480 lines
15 KiB
Go
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)
|
|
}
|