Introducing new PKI management

This commit is contained in:
nemunaire 2018-01-21 14:18:26 +01:00
parent 5b558bcf00
commit c118035c33
19 changed files with 857 additions and 53 deletions

View 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
}
}

View 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"`
}

View 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
}

View 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>

View 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)
}

133
admin/pki/ca.go Normal file
View File

@ -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
}

81
admin/pki/client.go Normal file
View File

@ -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()
}

76
admin/pki/common.go Normal file
View File

@ -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
}

View 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"))
})

View 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>

View 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
url: "/api/certs/" + certificate.id,
method: "PUT",
data: {
id_team: null
}
}).then(function(response) {
$scope.hasCertificate = true;
$rootScope.newBox('success', 'Team certificate successfully generated!');
$scope.certificates = TeamCertificate.query({ teamId: $routeParams.teamId });
$rootScope.newBox('success', 'Certificate successfully dissociated!');
}, 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
}).then(function(response) {
$scope.hasCertificate = false;
}, 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);
});
}

126
admin/static/views/pki.html Normal file
View File

@ -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>

View File

@ -69,15 +69,30 @@
<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>

View 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 {

View 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, fmt.Sprintf("_AUTH_ID_%s", team_id))); err != nil {
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(SubmissionDir, team_id)); err != nil {
log.Println("[ERR]", err)
}

View 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
}
}

View 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
}

View 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

View 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