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 }