server/admin/api/certificate.go

338 lines
9.9 KiB
Go

package api
import (
"crypto/rand"
"crypto/sha1"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base32"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"math"
"math/big"
"os"
"path"
"strconv"
"strings"
"time"
"srs.epita.fr/fic-server/admin/pki"
"srs.epita.fr/fic-server/libfic"
"github.com/julienschmidt/httprouter"
)
var TeamsDir string
func init() {
router.GET("/api/htpasswd", apiHandler(
func(httprouter.Params, []byte) (interface{}, error) {
return genHtpasswd(true)
}))
router.POST("/api/htpasswd", apiHandler(
func(httprouter.Params, []byte) (interface{}, error) {
if htpasswd, err := genHtpasswd(true); err != nil {
return nil, err
} else if err := ioutil.WriteFile(path.Join(pki.PKIDir, "shared", "ficpasswd"), []byte(htpasswd), 0644); err != nil {
return nil, err
} else {
return true, nil
}
}))
router.DELETE("/api/htpasswd", apiHandler(
func(httprouter.Params, []byte) (interface{}, error) {
if err := os.Remove(path.Join(pki.PKIDir, "shared", "ficpasswd")); err != nil {
return nil, err
} else {
return true, nil
}
}))
router.GET("/api/htpasswd.apr1", apiHandler(
func(httprouter.Params, []byte) (interface{}, error) {
return genHtpasswd(false)
}))
router.GET("/api/ca/", apiHandler(infoCA))
router.GET("/api/ca.pem", apiHandler(getCAPEM))
router.POST("/api/ca/new", apiHandler(
func(_ httprouter.Params, body []byte) (interface{}, error) {
var upki PKISettings
if err := json.Unmarshal(body, &upki); err != nil {
return nil, err
}
return true, pki.GenerateCA(upki.NotBefore, upki.NotAfter)
}))
router.GET("/api/teams/:tid/certificates", apiHandler(teamHandler(
func(team *fic.Team, _ []byte) (interface{}, error) {
if serials, err := pki.GetTeamSerials(TeamsDir, team.Id); err != nil {
return nil, err
} 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)
}
}
return certs, nil
}
})))
router.GET("/api/teams/:tid/associations", apiHandler(teamHandler(
func(team *fic.Team, _ []byte) (interface{}, error) {
return pki.GetTeamAssociations(TeamsDir, team.Id)
})))
router.POST("/api/teams/:tid/associations/:assoc", apiHandler(teamAssocHandler(
func(team *fic.Team, assoc string, _ []byte) (interface{}, error) {
if err := os.Symlink(fmt.Sprintf("%d", team.Id), path.Join(TeamsDir, assoc)); err != nil {
return nil, err
}
return "\"" + assoc + "\"", nil
})))
router.DELETE("/api/teams/:tid/associations/:assoc", apiHandler(teamAssocHandler(
func(team *fic.Team, assoc string, _ []byte) (interface{}, error) {
if err := os.Remove(path.Join(TeamsDir, assoc)); err != nil {
return nil, err
}
return "null", nil
})))
router.GET("/api/certs/", apiHandler(getCertificates))
router.POST("/api/certs/", apiHandler(generateClientCert))
router.DELETE("/api/certs/", apiHandler(func(_ httprouter.Params, _ []byte) (interface{}, error) { return fic.ClearCertificates() }))
router.HEAD("/api/certs/:certid", apiHandler(certificateHandler(getTeamP12File)))
router.GET("/api/certs/:certid", apiHandler(certificateHandler(getTeamP12File)))
router.PUT("/api/certs/:certid", apiHandler(certificateHandler(updateCertificateAssociation)))
router.DELETE("/api/certs/:certid", apiHandler(certificateHandler(
func(cert *fic.Certificate, _ []byte) (interface{}, error) { return cert.Revoke() })))
}
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(_ httprouter.Params, _ []byte) (interface{}, error) {
_, cacert, err := pki.LoadCA()
if err != nil {
return nil, err
}
return 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,
}, nil
}
func getCAPEM(_ httprouter.Params, _ []byte) (interface{}, error) {
if _, err := os.Stat(pki.CACertPath()); os.IsNotExist(err) {
return nil, errors.New("Unable to locate the CA root certificate. Have you generated it?")
} else if fd, err := os.Open(pki.CACertPath()); err != nil {
return nil, err
} else {
defer fd.Close()
return ioutil.ReadAll(fd)
}
}
func getTeamP12File(cert *fic.Certificate, _ []byte) (interface{}, error) {
// 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 {
return nil, err
}
}
if _, err := os.Stat(pki.ClientP12Path(cert.Id)); os.IsNotExist(err) {
return nil, errors.New("Unable to locate the p12. Have you generated it?")
} else if fd, err := os.Open(pki.ClientP12Path(cert.Id)); err != nil {
return nil, err
} else {
defer fd.Close()
return ioutil.ReadAll(fd)
}
}
func generateClientCert(_ httprouter.Params, _ []byte) (interface{}, error) {
// First, generate a new, unique, serial
var serial_gen [8]byte
if _, err := rand.Read(serial_gen[:]); err != nil {
return nil, err
}
for fic.ExistingCertSerial(serial_gen) {
if _, err := rand.Read(serial_gen[:]); err != nil {
return nil, err
}
}
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 {
return nil, err
}
// Ok, now load CA
capriv, cacert, err := pki.LoadCA()
if err != nil {
return nil, err
}
// Generate our privkey
if err := pki.GenerateClient(serial, cacert.NotBefore, cacert.NotAfter, &cacert, &capriv); err != nil {
return nil, err
}
// Save in DB
cert, err := fic.RegisterCertificate(serial, password)
return 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}, err
}
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(_ httprouter.Params, _ []byte) (interface{}, error) {
if certificates, err := fic.GetCertificates(); err != nil {
return nil, err
} else {
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})
}
return ret, nil
}
}
type CertUploaded struct {
Team *int64 `json:"id_team"`
}
func updateCertificateAssociation(cert *fic.Certificate, body []byte) (interface{}, error) {
var uc CertUploaded
if err := json.Unmarshal(body, &uc); err != nil {
return nil, err
}
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 {
return nil, err
}
// Mark team as active to ensure it'll be generated
if ut, err := fic.GetTeam(*uc.Team); err != nil {
return nil, err
} else if !ut.Active {
ut.Active = true
ut.Update()
}
} else {
os.Remove(dstLinkPath)
}
return cert, nil
}