package handlers import ( "bytes" "crypto/ecdsa" "crypto/rand" "crypto/rsa" "crypto/x509" "encoding/pem" "fmt" "html/template" "io/ioutil" "log" "math/big" "net/http" "strings" "time" "git.klink.asia/paul/certman/models" "git.klink.asia/paul/certman/services" "github.com/go-chi/chi" "git.klink.asia/paul/certman/views" ) func ListClientsHandler(p *services.Provider) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { v := views.NewWithSession(req, p.Sessions) username := p.Sessions.GetUsername(req) clients, _ := p.ClientCollection.ListClientsForUser(username) v.Vars["Clients"] = clients v.Render(w, "client_list") } } func CreateCertHandler(p *services.Provider) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { username := p.Sessions.GetUsername(req) certname := req.FormValue("certname") // Validate certificate Name if !IsByteLength(certname, 2, 64) || !IsDNSName(certname) { p.Sessions.Flash(w, req, services.Flash{ Type: "danger", Message: "The certificate name can only contain letters and numbers", }, ) http.Redirect(w, req, "/certs", http.StatusFound) return } // lowercase the certificate name, to avoid problems with the case // insensitive matching inside OpenVPN certname = strings.ToLower(certname) // Load CA master certificate caCert, caKey, err := loadX509KeyPair("ca.crt", "ca.key") if err != nil { log.Fatalf("error loading ca keyfiles: %s", err) panic(err.Error()) } // Generate Keypair key, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { log.Fatalf("Could not generate keypair: %s", err) p.Sessions.Flash(w, req, services.Flash{ Type: "danger", Message: "The certificate key could not be generated", }, ) http.Redirect(w, req, "/certs", http.StatusFound) return } // Generate Certificate commonName := fmt.Sprintf("%s@%s", username, certname) derBytes, err := CreateCertificate(commonName, key, caCert, caKey) // Initialize new client config client := models.Client{ Name: certname, CreatedAt: time.Now(), PrivateKey: x509.MarshalPKCS1PrivateKey(key), Cert: derBytes, User: username, } // Insert client into database if err := p.ClientCollection.CreateClient(&client); err != nil { log.Println(err.Error()) p.Sessions.Flash(w, req, services.Flash{ Type: "danger", Message: "The certificate could not be added to the database", }, ) http.Redirect(w, req, "/certs", http.StatusFound) return } p.Sessions.Flash(w, req, services.Flash{ Type: "success", Message: "The certificate was created successfully", }, ) http.Redirect(w, req, "/certs", http.StatusFound) } } func DeleteCertHandler(p *services.Provider) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { v := views.New(req) // detemine own username username := p.Sessions.GetUsername(req) name := chi.URLParam(req, "name") client, err := p.ClientCollection.GetClientByNameUser(name, username) if err != nil { v.RenderError(w, http.StatusNotFound) return } err = p.ClientCollection.DeleteClient(client.ID) if err != nil { p.Sessions.Flash(w, req, services.Flash{ Type: "danger", Message: "Failed to delete certificate", }, ) http.Redirect(w, req, "/certs", http.StatusFound) } p.Sessions.Flash(w, req, services.Flash{ Type: "success", Message: template.HTML(fmt.Sprintf("Successfully deleted client %s", client.Name)), }, ) http.Redirect(w, req, "/certs", http.StatusFound) } } func DownloadCertHandler(p *services.Provider) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { v := views.New(req) // detemine own username username := p.Sessions.GetUsername(req) name := chi.URLParam(req, "name") client, err := p.ClientCollection.GetClientByNameUser(name, username) if err != nil { v.RenderError(w, http.StatusNotFound) return } // cbuf and kbuf are buffers in which the PEM certificates are // rendered into var cbuf = new(bytes.Buffer) var kbuf = new(bytes.Buffer) pem.Encode(cbuf, &pem.Block{Type: "CERTIFICATE", Bytes: client.Cert}) pem.Encode(kbuf, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: client.PrivateKey}) vars := map[string]string{ "Cert": cbuf.String(), "Key": kbuf.String(), } t, err := views.GetTemplate("config.ovpn") if err != nil { log.Printf("Error loading certificate template: %s", err) v.RenderError(w, http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/x-openvpn-profile") w.Header().Set("Content-Disposition", "attachment; filename=\"config.ovpn\"") w.WriteHeader(http.StatusOK) t.Execute(w, vars) return } } func loadX509KeyPair(certFile, keyFile string) (*x509.Certificate, *rsa.PrivateKey, error) { cf, err := ioutil.ReadFile(certFile) if err != nil { return nil, nil, err } kf, err := ioutil.ReadFile(keyFile) if err != nil { return nil, nil, err } cpb, _ := pem.Decode(cf) kpb, _ := pem.Decode(kf) crt, err := x509.ParseCertificate(cpb.Bytes) if err != nil { return nil, nil, err } key, err := x509.ParsePKCS1PrivateKey(kpb.Bytes) if err != nil { return nil, nil, err } return crt, key, nil } // CreateCertificate creates a CA-signed certificate func CreateCertificate(commonName string, key interface{}, caCert *x509.Certificate, caKey interface{}) ([]byte, error) { subj := caCert.Subject // .. except for the common name subj.CommonName = commonName serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) if err != nil { log.Fatalf("Obscure error in cert serial number generation: %s", err) } template := x509.Certificate{ SerialNumber: serialNumber, Subject: subj, NotBefore: time.Now().Add(-5 * time.Minute), // account for clock shift NotAfter: time.Now().Add(24 * time.Hour * 356 * 5), // 5 years ought to be enough! SignatureAlgorithm: x509.SHA256WithRSA, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, BasicConstraintsValid: true, } return x509.CreateCertificate(rand.Reader, &template, caCert, publicKey(key), caKey) } func publicKey(priv interface{}) interface{} { switch k := priv.(type) { case *rsa.PrivateKey: return &k.PublicKey case *ecdsa.PrivateKey: return &k.PublicKey default: return nil } }