Introducing new PKI management
authornemunaire <nemunaire@nemunai.re>
Sun, 21 Jan 2018 13:18:26 +0000 (14:18 +0100)
committernemunaire <nemunaire@nemunai.re>
Fri, 11 May 2018 03:27:50 +0000 (05:27 +0200)
19 files changed:
admin/api/certificate.go
admin/api/claim.go
admin/api/handlers.go
admin/index.go
admin/main.go
admin/pki/ca.go [new file with mode: 0644]
admin/pki/client.go [new file with mode: 0644]
admin/pki/common.go [new file with mode: 0644]
admin/static.go
admin/static/index.html
admin/static/js/app.js
admin/static/views/pki.html [new file with mode: 0644]
admin/static/views/team-edit.html
backend/main.go
backend/registration.go
libfic/certificate.go
libfic/db.go
libfic/team.go
libfic/todo.go

index f4caee9e02af8b08a66b74ad49bdeeb37065fca9..69e762e22f6855396915ab76b821a521ad849652 100644 (file)
@@ -1,7 +1,167 @@
 package api
 
 import (
+       "encoding/json"
+       "errors"
+       "fmt"
+       "math/rand"
+       "io/ioutil"
+       "os"
+       "path"
+       "time"
+
+       "srs.epita.fr/fic-server/admin/pki"
+       "srs.epita.fr/fic-server/libfic"
+
+       "github.com/julienschmidt/httprouter"
 )
 
 func init() {
+       router.GET("/api/ca/", apiHandler(infoCA))
+       router.GET("/api/ca.pem", apiHandler(getCAPEM))
+       router.POST("/api/ca/new", apiHandler(
+               func(_ httprouter.Params, _ []byte) (interface{}, error) { return true, pki.GenerateCA(time.Date(2018, 01, 21, 0, 0, 0, 0, time.UTC), time.Date(2018, 01, 24, 23, 59, 59, 0, time.UTC)) }))
+
+       router.GET("/api/teams/:tid/certificates", apiHandler(teamHandler(
+               func(team fic.Team, _ []byte) (interface{}, error) { return fic.GetTeamCertificates(team) })))
+
+       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 infoCA(_ httprouter.Params, _ []byte) (interface{}, error) {
+       _, cacert, err := pki.LoadCA()
+       if err != nil {
+               return nil, err
+       }
+
+       ret := map[string]interface{}{}
+
+       ret["version"] = cacert.Version
+       ret["serialnumber"] = cacert.SerialNumber
+       ret["issuer"] = cacert.Issuer
+       ret["subject"] = cacert.Subject
+       ret["notbefore"] = cacert.NotBefore
+       ret["notafter"] = cacert.NotAfter
+       ret["signatureAlgorithm"] = cacert.SignatureAlgorithm
+       ret["publicKeyAlgorithm"] = cacert.PublicKeyAlgorithm
+
+       return ret, 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
+       serial := rand.Int63()
+       for fic.ExistingCertSerial(serial) {
+               serial = rand.Int63()
+       }
+
+       // Let's pick a random password
+       password, err := pki.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
+       return fic.RegisterCertificate(serial, password)
+}
+
+type CertExported struct {
+       Id       string     `json:"id"`
+       Creation time.Time  `json:"creation"`
+       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 _, c := range certificates {
+                       ret = append(ret, CertExported{fmt.Sprintf("%d", c.Id), c.Creation, c.IdTeam, c.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
+       }
+
+       // TODO: This should be read from file system, not in DB:
+       // the relation is made through a symlink, so if it exists, it is suffisant to read the relation
+       // moreover, backend doesn't update the DB at registration, it only creates a symlink
+       cert.IdTeam = uc.Team
+
+       dstLinkPath := path.Join(TeamsDir, fmt.Sprintf("_AUTH_ID_%X", cert.Id))
+
+       if uc.Team != nil {
+               srcLinkPath := fmt.Sprintf("%d", *uc.Team)
+               if err := os.Symlink(srcLinkPath, dstLinkPath); err != nil {
+                       return nil, err
+               }
+       } else {
+               os.Remove(dstLinkPath)
+       }
+
+       if _, err := cert.Update(); err != nil {
+               return nil, err
+       } else {
+               return cert, err
+       }
 }
index a5e815fc5c0ff797c7461fe0c22b29c82a77c79d..4145a2c00dee8d3618ad0b482cac646d2700ff0c 100644 (file)
@@ -79,7 +79,7 @@ func showClaim(claim fic.Claim, _ []byte) (interface{}, error) {
 
 type ClaimUploaded struct {
        Subject  string `json:"subject"`
-       Team     *int   `json:"id_team"`
+       Team     *int64 `json:"id_team"`
        Assignee *int64 `json:"id_assignee"`
        Priority string `json:"priority"`
 }
index b61d2d5025e46ba1962eaea0343fc20d84d919c5..8824b567e00c455f5fa6f608a2599bc84d43a864 100644 (file)
@@ -84,7 +84,7 @@ func apiHandler(f DispatchFunction) func(http.ResponseWriter, *http.Request, htt
 
 func teamPublicHandler(f func(*fic.Team,[]byte) (interface{}, error)) func (httprouter.Params,[]byte) (interface{}, error) {
        return func (ps httprouter.Params, body []byte) (interface{}, error) {
-               if tid, err := strconv.Atoi(string(ps.ByName("tid"))); err != nil {
+               if tid, err := strconv.ParseInt(string(ps.ByName("tid")), 10, 64); err != nil {
                        return nil, err
                } else if tid == 0 {
                        return f(nil, body)
@@ -98,7 +98,7 @@ func teamPublicHandler(f func(*fic.Team,[]byte) (interface{}, error)) func (http
 
 func teamHandler(f func(fic.Team,[]byte) (interface{}, error)) func (httprouter.Params,[]byte) (interface{}, error) {
        return func (ps httprouter.Params, body []byte) (interface{}, error) {
-               if tid, err := strconv.Atoi(string(ps.ByName("tid"))); err != nil {
+               if tid, err := strconv.ParseInt(string(ps.ByName("tid")), 10, 64); err != nil {
                        return nil, err
                } else if team, err := fic.GetTeam(tid); err != nil {
                        return nil, err
@@ -280,6 +280,18 @@ func fileHandler(f func(fic.EFile,[]byte) (interface{}, error)) func (httprouter
        }
 }
 
+func certificateHandler(f func(fic.Certificate,[]byte) (interface{}, error)) func (httprouter.Params,[]byte) (interface{}, error) {
+       return func (ps httprouter.Params, body []byte) (interface{}, error) {
+               if certid, err := strconv.ParseInt(string(ps.ByName("certid")), 10, 64); err != nil {
+                       return nil, err
+               } else if cert, err := fic.GetCertificate(certid); err != nil {
+                       return nil, err
+               } else {
+                       return f(cert, body)
+               }
+       }
+}
+
 func notFound(ps httprouter.Params, _ []byte) (interface{}, error) {
        return nil, nil
 }
index 939b925073ec8ccd38697b0bda610d5af5a544ce..989831c60571c4befe2392af91b75c9c7d5b6b92 100644 (file)
@@ -31,6 +31,7 @@ const indextpl = `<!DOCTYPE html>
       <div class="collapse navbar-collapse" id="adminMenu">
        <ul class="navbar-nav mr-auto">
          <li class="nav-item"><a class="nav-link" href="{{.urlbase}}teams">&Eacute;quipes</a></li>
+         <li class="nav-item"><a class="nav-link" href="{{.urlbase}}pki">PKI</a></li>
          <li class="nav-item"><a class="nav-link" href="{{.urlbase}}themes">Thèmes</a></li>
          <li class="nav-item"><a class="nav-link" href="{{.urlbase}}exercices">Exercices</a></li>
          <li class="nav-item"><a class="nav-link" href="{{.urlbase}}files">Fichiers</a></li>
index e2aa505552dbbc7bbde95cd1e1be05bdf1faa68d..8de70c29a3e58fee93beeda440e47c157631ca93 100644 (file)
@@ -13,6 +13,7 @@ import (
        "text/template"
 
        "srs.epita.fr/fic-server/admin/api"
+       "srs.epita.fr/fic-server/admin/pki"
        "srs.epita.fr/fic-server/admin/sync"
        "srs.epita.fr/fic-server/libfic"
        "srs.epita.fr/fic-server/settings"
@@ -68,6 +69,12 @@ func main() {
        localImporterSymlink := false
 
        // Read paremeters from environment
+       if v, exists := os.LookupEnv("FICCA_PASS"); exists {
+               pki.SetCAPassword(v)
+       } else {
+               log.Println("WARNING: no password defined for the CA, will use empty password to secure CA private key")
+               log.Println("WARNING:    PLEASE DEFINED ENVIRONMENT VARIABLE: FICCA_PASS")
+       }
        if v, exists := os.LookupEnv("FICCLOUD_URL"); exists {
                cloudDAVBase = v
        }
@@ -82,6 +89,7 @@ func main() {
        var bind = flag.String("bind", "127.0.0.1:8081", "Bind port/socket")
        var dsn = flag.String("dsn", fic.DSNGenerator(), "DSN to connect to the MySQL server")
        var baseURL = flag.String("baseurl", "/", "URL prepended to each URL")
+       flag.StringVar(&pki.PKIDir, "pki", "./PKI", "Base directory where found PKI scripts")
        flag.StringVar(&StaticDir, "static", "./htdocs-admin/", "Directory containing static files")
        flag.StringVar(&api.TeamsDir, "teams", "./TEAMS", "Base directory where save teams JSON files")
        flag.StringVar(&settings.SettingsDir, "settings", settings.SettingsDir, "Base directory where load and save settings")
@@ -122,6 +130,9 @@ func main() {
        if fic.FilesDir, err = filepath.Abs(fic.FilesDir); err != nil {
                log.Fatal(err)
        }
+       if pki.PKIDir, err = filepath.Abs(pki.PKIDir); err != nil {
+               log.Fatal(err)
+       }
        if api.TeamsDir, err = filepath.Abs(api.TeamsDir); err != nil {
                log.Fatal(err)
        }
@@ -139,7 +150,7 @@ func main() {
                baseURL = &tmp
        }
 
-       log.Println("Opening database...")
+       log.Println("Opening database...")
        if err := fic.DBInit(*dsn); err != nil {
                log.Fatal("Cannot open the database: ", err)
        }
diff --git a/admin/pki/ca.go b/admin/pki/ca.go
new file mode 100644 (file)
index 0000000..4ed4006
--- /dev/null
@@ -0,0 +1,133 @@
+package pki
+
+import (
+       "crypto/ecdsa"
+       "crypto/rand"
+       "crypto/x509"
+       "crypto/x509/pkix"
+       "encoding/pem"
+       "errors"
+       "io/ioutil"
+       "math/big"
+       "os"
+       "path"
+       "time"
+)
+
+var passwordCA string
+
+func SetCAPassword(pass string) {
+       passwordCA = pass
+}
+
+func CACertPath() string {
+       return path.Join(PKIDir, "shared", "ca.pem")
+}
+
+func CAPrivkeyPath() string {
+       return path.Join(PKIDir, "ca.key")
+}
+
+func GenerateCA(notBefore time.Time, notAfter time.Time) error {
+       ca := &x509.Certificate{
+               SerialNumber: big.NewInt(0),
+               Subject: pkix.Name{
+                       Organization:       []string{"EPITA"},
+                       OrganizationalUnit: []string{"SRS laboratory"},
+                       Country:            []string{"FR"},
+                       Locality:           []string{"Paris"},
+                       CommonName:         "FIC CA",
+               },
+               NotBefore:             notBefore,
+               NotAfter:              notAfter,
+               IsCA:                  true,
+               ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
+               KeyUsage:              x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
+               BasicConstraintsValid: true,
+       }
+
+       // Ensure directories exists
+       os.Mkdir(PKIDir, 0777)
+       os.Mkdir(path.Join(PKIDir, "shared"), 0777)
+
+       pub, priv, err := GeneratePrivKey()
+       if err != nil {
+               return err
+       }
+
+       ca_b, err := x509.CreateCertificate(rand.Reader, ca, ca, pub, priv)
+       if err != nil {
+               return err
+       }
+
+       // Save certificate to file
+       if err := saveCertificate(CACertPath(), ca_b); err != nil {
+               return err
+       }
+
+       // Save private key to file
+       if err := savePrivateKeyEncrypted(CAPrivkeyPath(), priv, passwordCA); err != nil {
+               return err
+       }
+
+       return nil
+}
+
+func LoadCA() (priv ecdsa.PrivateKey, ca x509.Certificate, err error) {
+       // Load certificate
+       if fd, errr := os.Open(CACertPath()); err != nil {
+               return priv, ca, errr
+       } else {
+               defer fd.Close()
+               if cert, errr := ioutil.ReadAll(fd); err != nil {
+                       return priv, ca, errr
+               } else {
+                       block, _ := pem.Decode(cert)
+                       if block == nil || block.Type != "CERTIFICATE" {
+                               return priv, ca, errors.New("failed to decode PEM block containing certificate")
+                       }
+                       if catmp, errr := x509.ParseCertificate(block.Bytes); err != nil {
+                               return priv, ca, errr
+                       } else if catmp == nil {
+                               return priv, ca, errors.New("failed to parse certificate")
+                       } else {
+                               ca = *catmp
+                       }
+               }
+       }
+
+       // Load private key
+       if fd, errr := os.Open(CAPrivkeyPath()); err != nil {
+               return priv, ca, errr
+       } else {
+               defer fd.Close()
+               if privkey, errr := ioutil.ReadAll(fd); err != nil {
+                       return priv, ca, errr
+               } else {
+                       block, _ := pem.Decode(privkey)
+                       if block == nil || block.Type != "EC PRIVATE KEY" {
+                               return priv, ca, errors.New("failed to decode PEM block containing EC private key")
+                       }
+
+                       var decrypted_der []byte
+                       if x509.IsEncryptedPEMBlock(block) {
+                               decrypted_der, err = x509.DecryptPEMBlock(block, []byte(passwordCA))
+                               if err != nil {
+                                       return
+                               }
+                       } else {
+                               decrypted_der = block.Bytes
+                       }
+
+                       if tmppriv, errr := x509.ParseECPrivateKey(decrypted_der); err != nil {
+                               return priv, ca, errr
+                       } else if tmppriv == nil {
+                               return priv, ca, errors.New("failed to parse private key")
+                       } else {
+                               priv = *tmppriv
+                       }
+               }
+       }
+
+       return
+}
diff --git a/admin/pki/client.go b/admin/pki/client.go
new file mode 100644 (file)
index 0000000..40c2ff4
--- /dev/null
@@ -0,0 +1,81 @@
+package pki
+
+import (
+       "crypto/ecdsa"
+       "crypto/rand"
+       "crypto/x509"
+       "crypto/x509/pkix"
+       "fmt"
+       "math/big"
+       "os"
+       "os/exec"
+       "path"
+       "time"
+)
+
+func ClientCertificatePath(serial int64) string {
+       return path.Join(PKIDir, fmt.Sprintf("%d", serial), "cert.pem")
+}
+
+func ClientPrivkeyPath(serial int64) string {
+       return path.Join(PKIDir, fmt.Sprintf("%d", serial), "privkey.pem")
+}
+
+func ClientP12Path(serial int64) string {
+       return path.Join(PKIDir, fmt.Sprintf("%d", serial), "team.p12")
+}
+
+func GenerateClient(serial int64, notBefore time.Time, notAfter time.Time, parent_cert *x509.Certificate, parent_priv *ecdsa.PrivateKey) error {
+       client := &x509.Certificate{
+               SerialNumber: big.NewInt(serial),
+               Subject: pkix.Name{
+                       Organization:       []string{"EPITA"},
+                       OrganizationalUnit: []string{"SRS laboratory"},
+                       Country:            []string{"FR"},
+                       Locality:           []string{"Paris"},
+                       CommonName:         fmt.Sprintf("TEAM-%o", serial),
+               },
+               NotBefore:             notBefore,
+               NotAfter:              notAfter,
+               IsCA:                  false,
+               ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
+               KeyUsage:              x509.KeyUsageDigitalSignature,
+               BasicConstraintsValid: true,
+       }
+
+       pub, priv, err := GeneratePrivKey()
+       if err != nil {
+               return err
+       }
+
+       client_b, err := x509.CreateCertificate(rand.Reader, client, parent_cert, pub, parent_priv)
+       if err != nil {
+               return err
+       }
+
+       // Create intermediate directory
+       os.MkdirAll(path.Join(PKIDir, fmt.Sprintf("%d", serial)), 0777)
+
+       // Save certificate to file
+       if err := saveCertificate(ClientCertificatePath(serial), client_b); err != nil {
+               return err
+       }
+
+       // Save private key to file
+       if err := savePrivateKey(ClientPrivkeyPath(serial), priv); err != nil {
+               return err
+       }
+
+       return nil
+}
+
+func WriteP12(serial int64, password string) error {
+       cmd := exec.Command("/usr/bin/openssl", "pkcs12", "-export",
+               "-inkey", ClientPrivkeyPath(serial),
+               "-in", ClientCertificatePath(serial),
+               "-name", fmt.Sprintf("TEAM-%o", serial),
+               "-passout", "pass:" + password,
+               "-out", ClientP12Path(serial))
+
+       return cmd.Run()
+}
diff --git a/admin/pki/common.go b/admin/pki/common.go
new file mode 100644 (file)
index 0000000..df134a3
--- /dev/null
@@ -0,0 +1,76 @@
+package pki
+
+import (
+       "crypto/ecdsa"
+       "crypto/elliptic"
+       "crypto/rand"
+       "crypto/x509"
+       "encoding/base64"
+       "encoding/pem"
+       "os"
+)
+
+var PKIDir string
+
+func GeneratePassword() (password string, err error) {
+       // This will make a 12 chars long password
+       b := make([]byte, 9)
+
+       if _, err = rand.Read(b); err != nil {
+               return
+       }
+
+       password = base64.StdEncoding.EncodeToString(b)
+
+       return
+}
+
+func GeneratePrivKey() (pub *ecdsa.PublicKey, priv *ecdsa.PrivateKey, err error) {
+       if priv, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader); err == nil {
+               pub = &priv.PublicKey
+       }
+       return
+}
+
+func saveCertificate(path string, cert []byte) error {
+       if certOut, err := os.Create(path); err != nil {
+               return err
+       } else {
+               pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: cert})
+               certOut.Close()
+       }
+       return nil
+}
+
+func savePrivateKey(path string, private *ecdsa.PrivateKey) error {
+       if keyOut, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600); err != nil {
+               return err
+       } else if key_b, err := x509.MarshalECPrivateKey(private); err != nil {
+               return err
+       } else {
+               pem.Encode(keyOut, &pem.Block{Type: "EC PRIVATE KEY", Bytes: key_b})
+               keyOut.Close()
+       }
+       return nil
+}
+
+func savePrivateKeyEncrypted(path string, private *ecdsa.PrivateKey, password string) error {
+       if password == "" {
+               return savePrivateKey(path, private)
+       }
+
+       if keyOut, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600); err != nil {
+               return err
+       } else {
+               defer keyOut.Close()
+
+               if key_b, err := x509.MarshalECPrivateKey(private); err != nil {
+                       return err
+               } else if key_c, err := x509.EncryptPEMBlock(rand.Reader, "EC PRIVATE KEY", key_b, []byte(password), x509.PEMCipherAES256); err != nil {
+                       return err
+               } else {
+                       pem.Encode(keyOut, key_c)
+               }
+       }
+       return nil
+}
index 6a62d747edfbef5f2a354190184f061376c12ba3..3362d45edb1bbcb68a2ee9ca6b9ecf42536309b2 100644 (file)
@@ -28,6 +28,9 @@ func init() {
        api.Router().GET("/public/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
                http.ServeFile(w, r, path.Join(StaticDir, "index.html"))
        })
+       api.Router().GET("/pki/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
+               http.ServeFile(w, r, path.Join(StaticDir, "index.html"))
+       })
        api.Router().GET("/settings/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
                http.ServeFile(w, r, path.Join(StaticDir, "index.html"))
        })
index 85900057748c622cf9582ae776b96d2101480a0b..4e5b1f2a73cc7220131f9a6a7e20d75bc8b32177 100644 (file)
@@ -29,6 +29,7 @@
       <div class="collapse navbar-collapse" id="adminMenu">
        <ul class="navbar-nav mr-auto">
          <li class="nav-item"><a class="nav-link" href="/teams">&Eacute;quipes</a></li>
+         <li class="nav-item"><a class="nav-link" href="/pki">PKI</a></li>
          <li class="nav-item"><a class="nav-link" href="/themes">Thèmes</a></li>
          <li class="nav-item"><a class="nav-link" href="/exercices">Exercices</a></li>
          <li class="nav-item"><a class="nav-link" href="/files">Fichiers</a></li>
index 986c1e9e50ab9fd0e91394a821bc44393fdf297a..b8391160df3f57a5515b5611608d3c522330dca2 100644 (file)
@@ -17,6 +17,10 @@ angular.module("FICApp", ["ngRoute", "ngResource", "ngSanitize"])
                controller: "SettingsController",
                templateUrl: "views/settings.html"
            })
+           .when("/pki", {
+               controller: "PKIController",
+               templateUrl: "views/pki.html"
+           })
            .when("/exercices", {
                controller: "AllExercicesListController",
                templateUrl: "views/exercice-list.html"
@@ -132,6 +136,14 @@ angular.module("FICApp")
            'update': {method: 'PUT'},
        })
     })
+    .factory("Certificate", function($resource) {
+       return $resource("/api/certs/:serial", { serial: '@id' }, {
+           'update': {method: 'PUT'},
+       })
+    })
+    .factory("CACertificate", function($resource) {
+       return $resource("/api/ca/:serial", { serial: '@id' })
+    })
     .factory("File", function($resource) {
        return $resource("/api/files/:fileId", { fileId: '@id' })
     })
@@ -153,6 +165,9 @@ angular.module("FICApp")
            'update': {method: 'PUT'},
        })
     })
+    .factory("TeamCertificate", function($resource) {
+       return $resource("/api/teams/:teamId/certificates", { teamId: '@id' })
+    })
     .factory("TeamMember", function($resource) {
        return $resource("/api/teams/:teamId/members", { teamId: '@id' }, {
            'save': {method: 'PUT'},
@@ -451,6 +466,56 @@ angular.module("FICApp")
        };
     })
 
+    .controller("PKIController", function($scope, $rootScope, Certificate, CACertificate, Team, $location, $http) {
+       $scope.teams = Team.query();
+       $scope.certificates = Certificate.query();
+       $scope.ca = CACertificate.get();
+
+       $scope.revoke = function() {
+           var targetserial = $("#revokeModal").data("certificate");
+           if (targetserial) {
+               Certificate.delete({ serial: targetserial }).$promise.then(
+                   function() {
+                       $('#revokeModal').modal('hide');
+                       $scope.certificates = Certificate.query();
+                   }, function(response) {
+                       $rootScope.newBox('danger', 'An error occurs when trying to associate certificate:', response.data);
+                   }
+               );
+           }
+       };
+
+       $scope.associate = function() {
+           var targetserial = $("#associationModal").data("certificate");
+           if (!targetserial) return;
+           Certificate.update({ serial: targetserial }, { id_team: $scope.selectedTeam }).$promise.then(
+               function() {
+                   $('#associationModal').modal('hide');
+                   $scope.certificates = Certificate.query();
+                   $scope.selectedTeam = null;
+               }, function(response) {
+                   $rootScope.newBox('danger', 'An error occurs when trying to associate certificate:', response.data);
+               }
+           );
+       };
+
+       $scope.generateCA = function() {
+           $http.post("/api/ca/new").then(function() {
+               $scope.ca = CACertificate.get();
+           }, function(response) {
+               $rootScope.newBox('danger', 'An error occurs when generating CA:', response.data);
+           });
+       };
+
+       $scope.generateCert = function() {
+           $http.post("/api/certs").then(function() {
+               $scope.certificates = Certificate.query();
+           }, function(response) {
+               $rootScope.newBox('danger', 'An error occurs when generating certificate:', response.data);
+           });
+       };
+    })
+
     .controller("PublicController", function($scope, $rootScope, $routeParams, $location, Scene, Theme, Teams, Exercice) {
        $scope.screens = [0,1,2,3,4,5,6,7,8,9];
        $scope.screenid = $routeParams.screenId;
@@ -1068,47 +1133,26 @@ angular.module("FICApp")
            }
        }
     })
-    .controller("TeamController", function($scope, $rootScope, $location, Team, TeamMember, $routeParams, $http) {
+    .controller("TeamController", function($scope, $rootScope, $location, Team, TeamMember, TeamCertificate, $routeParams, $http) {
        if ($scope.team && $scope.team.id)
            $routeParams.teamId = $scope.team.id;
        $scope.team = Team.get({ teamId: $routeParams.teamId });
        $scope.fields = ["name", "color"];
 
-       $scope.hasCertificate = false;
-       $http({
-           url: "/api/teams/" + Math.floor($routeParams.teamId) + "/certificate.p12",
-           method: "HEAD",
-           transformResponse: null
-       }).then(function(response) {
-           $scope.hasCertificate = true;
-       }, function(response) {
-           $scope.hasCertificate = false;
-       });
+       $scope.certificates = TeamCertificate.query({ teamId: $routeParams.teamId });
 
-       $scope.generateCertificate = function() {
+       $scope.dissociateCertificate = function(certificate) {
            $http({
-               url: "/api/teams/" + Math.floor($routeParams.teamId) + "/certificate/generate",
-               method: "POST",
-               transformResponse: null
-           }).then(function(response) {
-               $scope.hasCertificate = true;
-               $rootScope.newBox('success', 'Team certificate successfully generated!');
-           }, function(response) {
-               $rootScope.newBox('danger', 'An error occurs when generating certiticate:', response.data);
-           });
-       }
-       $scope.revokeCertificate = function() {
-           if (!confirm("Are you sure you want to revoke this certificate?"))
-               return false;
-
-           $http({
-               url: "/api/teams/" + Math.floor($routeParams.teamId) + "/certificate.p12",
-               method: "DELETE",
-               transformResponse: null
+               url: "/api/certs/" + certificate.id,
+               method: "PUT",
+               data: {
+                   id_team: null
+               }
            }).then(function(response) {
-               $scope.hasCertificate = false;
+               $scope.certificates = TeamCertificate.query({ teamId: $routeParams.teamId });
+               $rootScope.newBox('success', 'Certificate successfully dissociated!');
            }, function(response) {
-               $rootScope.newBox('danger', 'An error occurs when revoking the certiticate:', response.data);
+               $rootScope.newBox('danger', 'An error occurs when dissociating certiticate:', response.data);
            });
        }
 
diff --git a/admin/static/views/pki.html b/admin/static/views/pki.html
new file mode 100644 (file)
index 0000000..c5a7b4d
--- /dev/null
@@ -0,0 +1,126 @@
+<h2>
+  Certificats clients
+  <button ng-click="generateCert()" class="float-right btn btn-sm btn-primary" style="margin-right: 10px"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Générer un certificat</button>
+</h2>
+
+<p><input type="search" class="form-control" placeholder="Search" ng-model="query" autofocus></p>
+<table class="table table-hover table-bordered table-striped table-sm">
+  <thead class="thead-dark">
+    <tr>
+      <th>Serial</th>
+      <th>Date de création</th>
+      <th>Équipe</th>
+      <th>Révoqué ?</th>
+      <th>Action</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr ng-repeat="certificate in certificates | filter: query" ng-click="show(certificate.id)">
+      <td>{{ certificate.id }}</td>
+      <td>{{ certificate.creation }}</td>
+      <td ng-if="certificate.id_team">
+       <span ng-repeat="team in teams" ng-if="team.id == certificate.id_team">
+         <a ng-href="teams/{{ team.id }}">{{ team.name }}</a>
+       </span>
+      </td>
+      <td ng-if="!certificate.id_team">
+       <button type="button" class="btn btn-sm btn-primary" data-toggle="modal" data-target="#associationModal" data-certificate="{{ certificate.id }}"><span class="glyphicon glyphicon-link" aria-hidden="true"></span> Associer</button>
+      </td>
+      <td>{{ certificate.revoked }}</td>
+      <td>
+       <a type="button" class="btn btn-sm btn-success" href="api/certs/{{ certificate.id }}" target="_self">Télécharger</a>
+       <button type="button" class="btn btn-sm btn-danger" data-toggle="modal" data-target="#revokeModal" data-certificate="{{ certificate.id }}">Révoquer</button>
+      </td>
+    </tr>
+  </tbody>
+</table>
+
+<div class="modal fade" id="revokeModal" tabindex="-1" role="dialog">
+  <div class="modal-dialog" role="document">
+    <div class="modal-content">
+      <div class="modal-header">
+       <h5 class="modal-title">Révocation d'un certificat</h5>
+       <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+         <span aria-hidden="true">&times;</span>
+       </button>
+      </div>
+      <div class="modal-body">
+       <p>
+         Êtes-vous sûr de vouloir révoquer le certificat ?
+       </p>
+      </div>
+      <div class="modal-footer">
+       <button type="button" class="btn btn-secondary" data-dismiss="modal">Annuler</button>
+       <button type="button" class="btn btn-danger" ng-click="revoke()">Révoquer</button>
+      </div>
+    </div>
+  </div>
+</div>
+
+<script>
+  $('#revokeModal').on('shown.bs.modal', function (event) {
+    var button = $(event.relatedTarget);
+    var serial = button.data('certificate');
+
+    var modal = $(this);
+    modal.data('certificate', serial);
+  });
+</script>
+
+<div class="modal fade" id="associationModal" tabindex="-1" role="dialog">
+  <div class="modal-dialog" role="document">
+    <div class="modal-content">
+      <div class="modal-header">
+       <h5 class="modal-title">Associer le certificat à une équipe</h5>
+       <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+         <span aria-hidden="true">&times;</span>
+       </button>
+      </div>
+      <form ng-submit="associate()">
+       <div class="modal-body row">
+         <label for="tteam" class="col-md-auto col-form-label">Équipe</label>
+         <div class="col-md-auto">
+           <select class="custom-select custom-select-sm" id="tteam" ng-model="selectedTeam" ng-options="t.id as t.name for t in teams"></select>
+         </div>
+       </div>
+       <div class="modal-footer">
+         <button type="button" class="btn btn-secondary" data-dismiss="modal">Annuler</button>
+         <button type="submit" class="btn btn-primary">Associer</button>
+       </div>
+      </form>
+    </div>
+  </div>
+</div>
+
+<script>
+  $('#associationModal').on('shown.bs.modal', function (event) {
+    $('#tteam').trigger('focus');
+
+    var button = $(event.relatedTarget);
+    var serial = button.data('certificate');
+
+    var modal = $(this);
+    modal.data('certificate', serial);
+  });
+</script>
+
+<hr>
+
+<h2>
+  Autorité de certification
+  <span class="badge badge-success" ng-if="ca.version">Générée</span>
+  <span class="badge badge-danger" ng-if="!ca.version">Introuvable</span>
+  <button ng-click="generateCA()" class="float-right btn btn-sm btn-primary" ng-if="!ca.version"><span class="glyphicon glyphicon-certificate" aria-hidden="true"></span> Générer</button>
+</h2>
+
+<div class="alert alert-info" ng-if="!ca.version">
+  <strong>Aucune CA n'a été générée pour le moment.</strong>
+</div>
+
+<dl ng-if="ca.version">
+  <ng-repeat ng-repeat="(k, v) in ca">
+    <dt>{{ k }}</dt>
+    <dd ng-if="v.CommonName">/CN={{ v.CommonName }}/OU={{ v.OrganizationalUnit }}/O={{ v.Organization }}/L={{ v.Locality }}/P={{ v.Province }}/C={{ v.Country }}/</dd>
+    <dd ng-if="!v.CommonName">{{ v }}</dd>
+  </ng-repeat>
+</dl>
index 5910fa32845cf36165cca4bf58738780b79fdbe7..c63dbf03991a3b4ed833d9c3bfdf57826c6a6028 100644 (file)
   <div class="card">
     <div class="card-header bg-primary text-light">
       <span class="glyphicon glyphicon-certificate" aria-hidden="true"></span>
-      Certificate
-      <span class="badge badge-success" ng-if="hasCertificate">Generated</span>
-      <span class="badge badge-danger" ng-if="!hasCertificate">Not found</span>
+      Certificates
+      <span class="badge badge-success" ng-if="certificates.length">Generated</span>
+      <span class="badge badge-danger" ng-if="!certificates.length">Not found</span>
     </div>
     <div class="card-body bg-light text-dark">
-      <button ng-click="generateCertificate()" class="btn btn-success" ng-if="!hasCertificate">
-       <span class="glyphicon glyphicon-certificate" aria-hidden="true"></span> Generate certificate</button>
-      <button ng-click="revokeCertificate()" class="btn btn-danger" ng-if="hasCertificate">
-       <span class="glyphicon glyphicon-remove" aria-hidden="true"></span> Revoke certificate</button>
+      <dl class="dl-horizontal" ng-repeat="cert in certificates">
+       <dt>
+         Numéro de série
+         <button type="button" class="btn btn-sm btn-primary float-right" ng-click="dissociateCertificate(cert)">Dissocier</button>
+       </dt>
+       <dd>
+         {{ cert.id }}
+         <span class="badge badge-danger" ng-if="cert.revoked">Révoqué</span>
+       </dd>
+       <dt>
+         Date de création
+         <a class="btn btn-sm btn-success float-right" href="../api/certs/{{ cert.id }}">Télécharger</a>
+       </dt>
+       <dd>{{ cert.creation }}</dd>
+       <dt>Mot de passe</dt>
+       <dd>{{ cert.password }}</dd>
+       <dt ng-if="cert.revoked">Date de révocation</dt>
+       <dd ng-if="cert.revoked">{{ cert.revoked }}</dd>
+      </dl>
     </div>
   </div>
 
index 9085d146314e007ae48e07ee6b0f50f6f404af04..6a2062f2475973d15f4c8b48b4c037815fa4c146 100644 (file)
@@ -143,15 +143,33 @@ func treat(raw_path string) {
        if len(spath) == 3 {
                if spath[1] == "_registration" {
                        treatRegistration(raw_path, spath[2])
-               } else if teamid, err := strconv.Atoi(spath[1]); err != nil {
+                       return
+               }
+
+               var team fic.Team
+
+               if strings.HasPrefix(spath[1], "_AUTH_ID_") {
+                       if serial, err := strconv.ParseInt(strings.TrimPrefix(spath[1], "_AUTH_ID_"), 16, 64); err != nil {
+                               log.Println("[ERR]", err)
+                               return
+                       } else if team, err = fic.GetTeamBySerial(serial); err != nil {
+                               log.Println("[ERR]", err)
+                               return
+                       }
+               } else if teamid, err := strconv.ParseInt(spath[1], 10, 64); err != nil {
                        log.Println("[ERR]", err)
-               } else if team, err := fic.GetTeam(teamid); err != nil {
+                       return
+               } else if team, err = fic.GetTeam(teamid); err != nil {
                        log.Println("[ERR]", err)
-               } else if spath[2] == "name" {
+                       return
+               }
+
+               switch spath[2] {
+               case "name":
                        treatRename(raw_path, team)
-               } else if spath[2] == "hint" {
+               case "hint":
                        treatOpeningHint(raw_path, team)
-               } else {
+               default:
                        treatSubmission(raw_path, team, spath[2])
                }
        } else {
index 4cd4f9325467b27945716d7ae1edc59a5886f7d0..09cc5a36e6547654ca63ac229a637b82a4f1b849 100644 (file)
@@ -43,11 +43,21 @@ func treatRegistration(pathname string, team_id string) {
                                log.Println("[WRN] Unable to create event:", err)
                        }
 
-                       teamDirPath := path.Join(TeamsDir, fmt.Sprintf("%d", team.Id))
-                       if err := os.MkdirAll(teamDirPath, 0777); err != nil {
+                       teamDirPath := fmt.Sprintf("%d", team.Id)
+
+                       // Create team directories into TEAMS
+                       if err := os.MkdirAll(path.Join(TeamsDir, teamDirPath), 0777); err != nil {
+                               log.Println("[ERR]", err)
+                       }
+                       if err := os.Symlink(teamDirPath, path.Join(TeamsDir, team_id)); err != nil {
+                               log.Println("[ERR]", err)
+                       }
+
+                       // Create team directories into submissions
+                       if err := os.MkdirAll(path.Join(SubmissionDir, teamDirPath), 0777); err != nil {
                                log.Println("[ERR]", err)
                        }
-                       if err := os.Symlink(teamDirPath, path.Join(TeamsDir, fmt.Sprintf("_AUTH_ID_%s", team_id))); err != nil {
+                       if err := os.Symlink(teamDirPath, path.Join(SubmissionDir, team_id)); err != nil {
                                log.Println("[ERR]", err)
                        }
 
index e0f1795c67eec3e011720d0ee24ea5b16331cee3..cce949aa7073915441c349da803d1db3f5272940 100644 (file)
@@ -1,4 +1,105 @@
 package fic
 
 import (
+       "database/sql"
+       "time"
 )
+
+type Certificate struct {
+       Id       int64      `json:"id,string"`
+       Creation time.Time  `json:"creation"`
+       Password string     `json:"password"`
+       IdTeam   *int64     `json:"id_team"`
+       Revoked  *time.Time `json:"revoked"`
+}
+
+func GetCertificates() (certificates []Certificate, err error) {
+       var rows *sql.Rows
+       if rows, err = DBQuery("SELECT id_cert, creation, password, id_team, revoked FROM certificates ORDER BY creation"); err == nil {
+               defer rows.Close()
+
+               certificates = make([]Certificate, 0)
+               for rows.Next() {
+                       var c Certificate
+                       if err = rows.Scan(&c.Id, &c.Creation, &c.Password, &c.IdTeam, &c.Revoked); err != nil {
+                               return
+                       }
+                       certificates = append(certificates, c)
+               }
+               err = rows.Err()
+       }
+       return
+}
+
+func GetTeamCertificates(team Team) (certificates []Certificate, err error) {
+       var rows *sql.Rows
+       if rows, err = DBQuery("SELECT id_cert, creation, password, id_team, revoked FROM certificates WHERE id_team = ? ORDER BY creation", team.Id); err == nil {
+               defer rows.Close()
+
+               certificates = make([]Certificate, 0)
+               for rows.Next() {
+                       var c Certificate
+                       if err = rows.Scan(&c.Id, &c.Creation, &c.Password, &c.IdTeam, &c.Revoked); err != nil {
+                               return
+                       }
+                       certificates = append(certificates, c)
+               }
+               err = rows.Err()
+       }
+       return
+}
+
+func GetCertificate(serial int64) (c Certificate, err error) {
+       err = DBQueryRow("SELECT id_cert, creation, password, id_team, revoked FROM certificates WHERE id_cert = ?", serial).Scan(&c.Id, &c.Creation, &c.Password, &c.IdTeam, &c.Revoked)
+       return
+}
+
+func ExistingCertSerial(serial int64) (bool) {
+       c, _ := GetCertificate(serial)
+       return c.Id > 0
+}
+
+func RegisterCertificate(serial int64, password string) (Certificate, error) {
+       now := time.Now()
+       if _, err := DBExec("INSERT INTO certificates (id_cert, creation, password) VALUES (?, ?, ?)", serial, now, password); err != nil {
+               return Certificate{}, err
+       } else {
+               return Certificate{serial, now, password, nil, nil}, nil
+       }
+}
+
+func (c Certificate) Update() (int64, error) {
+       if res, err := DBExec("UPDATE certificates SET creation = ?, password = ?, id_team = ?, revoked = ? WHERE id_cert = ?", c.Creation, c.Password, c.IdTeam, c.Revoked, c.Id); err != nil {
+               return 0, err
+       } else if nb, err := res.RowsAffected(); err != nil {
+               return 0, err
+       } else {
+               return nb, err
+       }
+}
+
+func (c *Certificate) Revoke() (int64, error) {
+       now := time.Now()
+       c.Revoked = &now
+       return c.Update()
+}
+
+func (c Certificate) Delete() (int64, error) {
+       if res, err := DBExec("DELETE FROM certificates WHERE id_cert = ?", c.Id); err != nil {
+               return 0, err
+       } else if nb, err := res.RowsAffected(); err != nil {
+               return 0, err
+       } else {
+               return nb, err
+       }
+}
+
+func ClearCertificates() (int64, error) {
+       if res, err := DBExec("DELETE FROM certificates"); err != nil {
+               return 0, err
+       } else if nb, err := res.RowsAffected(); err != nil {
+               return 0, err
+       } else {
+               return nb, err
+       }
+}
index 14f0359c924817399a72fc0c4189b3117a9b82de..8c945cc48a5a0173a6a0fa08a153c4aaf97478ff 100644 (file)
@@ -79,6 +79,18 @@ CREATE TABLE IF NOT EXISTS teams(
   name VARCHAR(255) NOT NULL,
   color INTEGER NOT NULL
 ) DEFAULT CHARACTER SET = utf8 COLLATE = utf8_bin;
+`); err != nil {
+               return err
+       }
+       if _, err := db.Exec(`
+CREATE TABLE IF NOT EXISTS certificates(
+  id_cert BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT,
+  creation TIMESTAMP NOT NULL,
+  password VARCHAR(255) NOT NULL,
+  id_team INTEGER NULL,
+  revoked TIMESTAMP NULL,
+  FOREIGN KEY(id_team) REFERENCES teams(id_team)
+) DEFAULT CHARACTER SET = utf8 COLLATE = utf8_bin;
 `); err != nil {
                return err
        }
index c24951be266e4b59335fbf2a96b651f80c586c81..42d8b7edb1178883b9254bdd159dcafac47d01a7 100644 (file)
@@ -36,7 +36,7 @@ func GetTeams() ([]Team, error) {
        }
 }
 
-func GetTeam(id int) (Team, error) {
+func GetTeam(id int64) (Team, error) {
        var t Team
        if err := DBQueryRow("SELECT id_team, name, color FROM teams WHERE id_team = ?", id).Scan(&t.Id, &t.Name, &t.Color); err != nil {
                return t, err
index 1d2a64bd54a2e81b98def93587213f1544055d5d..0ac2486b0d0d761c966f5b0ab985be321d9641a6 100644 (file)
@@ -85,7 +85,7 @@ func NewClaim(subject string, team *Team, assignee *ClaimAssignee, priority stri
 func (c Claim) GetTeam() (*Team, error) {
        if c.IdTeam == nil {
                return nil, nil
-       } else if t, err := GetTeam(int(*c.IdTeam)); err != nil {
+       } else if t, err := GetTeam(*c.IdTeam); err != nil {
                return nil, err
        } else {
                return &t, nil