Move handlers into separate package
This commit is contained in:
		
							parent
							
								
									b748050de3
								
							
						
					
					
						commit
						08be6e68bc
					
				
					 4 changed files with 393 additions and 325 deletions
				
			
		
							
								
								
									
										98
									
								
								handlers/index.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								handlers/index.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,98 @@ | |||
| package handlers | ||||
| 
 | ||||
| import ( | ||||
| 	"html/template" | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	"github.com/gorilla/csrf" | ||||
| 	"github.com/nicksnyder/go-i18n/v2/i18n" | ||||
| 	log "github.com/sirupsen/logrus" | ||||
| ) | ||||
| 
 | ||||
| type IndexHandler struct { | ||||
| 	bundle *i18n.Bundle | ||||
| } | ||||
| 
 | ||||
| func NewIndexHandler(bundle *i18n.Bundle) *IndexHandler { | ||||
| 	return &IndexHandler{bundle: bundle} | ||||
| } | ||||
| 
 | ||||
| func (i *IndexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||||
| 	localizer := i18n.NewLocalizer(i.bundle, r.Header.Get("Accept-Language")) | ||||
| 	csrGenTitle := localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID:    "CSRGenTitle", | ||||
| 		Other: "CSR generation in browser", | ||||
| 	}}) | ||||
| 	nameLabel := localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID:    "NameLabel", | ||||
| 		Other: "Your name", | ||||
| 	}}) | ||||
| 	nameHelpText := localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID:    "NameHelpText", | ||||
| 		Other: "Please input your name as it should be added to your certificate", | ||||
| 	}}) | ||||
| 	passwordLabel := localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID:    "PasswordLabel", | ||||
| 		Other: "Password for your client certificate", | ||||
| 	}}) | ||||
| 	rsaKeySizeLegend := localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID:    "RSAKeySizeLabel", | ||||
| 		Other: "RSA Key Size", | ||||
| 	}}) | ||||
| 	rsa3072Label := localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID:    "RSA3072Label", | ||||
| 		Other: "3072 Bit", | ||||
| 	}}) | ||||
| 	rsa2048Label := localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID:    "RSA2048Label", | ||||
| 		Other: "2048 Bit (not recommended)", | ||||
| 	}}) | ||||
| 	rsa4096Label := localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID:    "RSA4096Label", | ||||
| 		Other: "4096 Bit", | ||||
| 	}}) | ||||
| 	rsaHelpText := localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID: "RSAHelpText", | ||||
| 		Other: "An RSA key pair will be generated in your browser. Longer key" + | ||||
| 			" sizes provide better security but take longer to generate.", | ||||
| 	}}) | ||||
| 	csrButtonLabel := localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID:    "CSRButtonLabel", | ||||
| 		Other: "Generate signing request", | ||||
| 	}}) | ||||
| 	statusLoading := localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID:    "StatusLoading", | ||||
| 		Other: "Loading ...", | ||||
| 	}}) | ||||
| 	downloadLabel := localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID:    "DownloadLabel", | ||||
| 		Other: "Download", | ||||
| 	}}) | ||||
| 	downloadDescription := localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID: "DownloadDescription", | ||||
| 		Other: "Your key material is ready for download. The downloadable file contains your private key and your" + | ||||
| 			" certificate encrypted with your password. You can now use the file to install your certificate in your" + | ||||
| 			" browser or other applications.", | ||||
| 	}}) | ||||
| 
 | ||||
| 	t := template.Must(template.ParseFiles("templates/index.html")) | ||||
| 	err := t.Execute(w, map[string]interface{}{ | ||||
| 		"Title":               csrGenTitle, | ||||
| 		"NameLabel":           nameLabel, | ||||
| 		"NameHelpText":        nameHelpText, | ||||
| 		"PasswordLabel":       passwordLabel, | ||||
| 		"RSAKeySizeLegend":    rsaKeySizeLegend, | ||||
| 		"RSA3072Label":        rsa3072Label, | ||||
| 		"RSA2048Label":        rsa2048Label, | ||||
| 		"RSA4096Label":        rsa4096Label, | ||||
| 		"RSAHelpText":         rsaHelpText, | ||||
| 		"CSRButtonLabel":      csrButtonLabel, | ||||
| 		"StatusLoading":       statusLoading, | ||||
| 		"DownloadDescription": downloadDescription, | ||||
| 		"DownloadLabel":       downloadLabel, | ||||
| 		csrf.TemplateTag:      csrf.TemplateField(r), | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		log.Panic(err) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										55
									
								
								handlers/jslocales.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								handlers/jslocales.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,55 @@ | |||
| package handlers | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/nicksnyder/go-i18n/v2/i18n" | ||||
| ) | ||||
| 
 | ||||
| type JSLocalesHandler struct { | ||||
| 	bundle *i18n.Bundle | ||||
| } | ||||
| 
 | ||||
| func NewJSLocalesHandler(bundle *i18n.Bundle) *JSLocalesHandler { | ||||
| 	return &JSLocalesHandler{bundle: bundle} | ||||
| } | ||||
| 
 | ||||
| func (j *JSLocalesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||||
| 	parts := strings.Split(r.URL.Path, "/") | ||||
| 	if len(parts) != 4 { | ||||
| 		http.Error(w, "Not found", http.StatusNotFound) | ||||
| 		return | ||||
| 	} | ||||
| 	lang := parts[2] | ||||
| 
 | ||||
| 	localizer := i18n.NewLocalizer(j.bundle, lang) | ||||
| 
 | ||||
| 	type translationData struct { | ||||
| 		Keygen struct { | ||||
| 			Started   string `json:"started"` | ||||
| 			Running   string `json:"running"` | ||||
| 			Generated string `json:"generated"` | ||||
| 		} `json:"keygen"` | ||||
| 	} | ||||
| 
 | ||||
| 	translations := &translationData{} | ||||
| 	translations.Keygen.Started = localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID:    "JavaScript.KeyGen.Started", | ||||
| 		Other: "started key generation", | ||||
| 	}}) | ||||
| 	translations.Keygen.Running = localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID:    "JavaScript.KeyGen.Running", | ||||
| 		Other: "key generation running for __seconds__ seconds", | ||||
| 	}}) | ||||
| 	translations.Keygen.Generated = localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID:    "JavaScript.KeyGen.Generated", | ||||
| 		Other: "key generated in __seconds__ seconds", | ||||
| 	}}) | ||||
| 
 | ||||
| 	encoder := json.NewEncoder(w) | ||||
| 	if err := encoder.Encode(translations); err != nil { | ||||
| 		http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										171
									
								
								handlers/signing.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										171
									
								
								handlers/signing.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,171 @@ | |||
| package handlers | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"crypto/x509" | ||||
| 	"encoding/json" | ||||
| 	"encoding/pem" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
| 	"os" | ||||
| 	"os/exec" | ||||
| 
 | ||||
| 	log "github.com/sirupsen/logrus" | ||||
| ) | ||||
| 
 | ||||
| type CertificateSigningHandler struct { | ||||
| 	caCertificates []*x509.Certificate | ||||
| 	caChainMap     map[string][]string | ||||
| } | ||||
| 
 | ||||
| func NewCertificateSigningHandler(caCertificates []*x509.Certificate) *CertificateSigningHandler { | ||||
| 	return &CertificateSigningHandler{caCertificates: caCertificates, caChainMap: make(map[string][]string)} | ||||
| } | ||||
| 
 | ||||
| func (h *CertificateSigningHandler) sign(csrPem string, commonName string) (certPem string, caChain []string, err error) { | ||||
| 	log.Printf("received CSR for %s:\n\n%s", commonName, csrPem) | ||||
| 	subjectDN := fmt.Sprintf("/CN=%s", commonName) | ||||
| 	var csrFile *os.File | ||||
| 	if csrFile, err = ioutil.TempFile("", "*.csr.pem"); err != nil { | ||||
| 		log.Errorf("could not open temporary file: %s", err) | ||||
| 		return | ||||
| 	} | ||||
| 	if _, err = csrFile.Write([]byte(csrPem)); err != nil { | ||||
| 		log.Errorf("could not write CSR to file: %s", err) | ||||
| 		return | ||||
| 	} | ||||
| 	if err = csrFile.Close(); err != nil { | ||||
| 		log.Errorf("could not close CSR file: %s", err) | ||||
| 		return | ||||
| 	} | ||||
| 	defer func(file *os.File) { | ||||
| 		err = os.Remove(file.Name()) | ||||
| 		if err != nil { | ||||
| 			log.Errorf("could not remove temporary file: %s", err) | ||||
| 		} | ||||
| 	}(csrFile) | ||||
| 
 | ||||
| 	opensslCommand := exec.Command( | ||||
| 		"openssl", "ca", "-config", "ca.cnf", | ||||
| 		"-policy", "policy_match", "-extensions", "client_ext", | ||||
| 		"-batch", "-subj", subjectDN, "-utf8", "-rand_serial", "-in", "in.pem") | ||||
| 	var out, cmdErr bytes.Buffer | ||||
| 	opensslCommand.Stdout = &out | ||||
| 	opensslCommand.Stderr = &cmdErr | ||||
| 	err = opensslCommand.Run() | ||||
| 	if err != nil { | ||||
| 		log.Print(err) | ||||
| 		log.Print(cmdErr.String()) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	var block *pem.Block | ||||
| 	if block, _ = pem.Decode(out.Bytes()); block == nil { | ||||
| 		err = fmt.Errorf("could not decode pem") | ||||
| 		return | ||||
| 	} | ||||
| 	var certificate *x509.Certificate | ||||
| 	if certificate, err = x509.ParseCertificate(block.Bytes); err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	certPem = string(pem.EncodeToMemory(&pem.Block{ | ||||
| 		Type:  "CERTIFICATE", | ||||
| 		Bytes: certificate.Raw, | ||||
| 	})) | ||||
| 
 | ||||
| 	caChain, err = h.getCAChain(certificate) | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
| func (h *CertificateSigningHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||||
| 	if r.Method != "POST" { | ||||
| 		http.Error(w, "Only POST requests support", http.StatusMethodNotAllowed) | ||||
| 		return | ||||
| 	} | ||||
| 	if r.Header.Get("content-type") != "application/json" { | ||||
| 		http.Error(w, "Only JSON content is accepted", http.StatusNotAcceptable) | ||||
| 		return | ||||
| 	} | ||||
| 	var err error | ||||
| 	var requestBody requestData | ||||
| 	var responseData responseData | ||||
| 
 | ||||
| 	if err = json.NewDecoder(r.Body).Decode(&requestBody); err != nil { | ||||
| 		log.Print(err) | ||||
| 		http.Error(w, err.Error(), http.StatusBadRequest) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	responseData.Certificate, responseData.CAChain, err = h.sign(requestBody.Csr, requestBody.CommonName) | ||||
| 	if err != nil { | ||||
| 		http.Error(w, "Could not sign certificate", http.StatusInternalServerError) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	var jsonBytes []byte | ||||
| 	if jsonBytes, err = json.Marshal(&responseData); err != nil { | ||||
| 		log.Print(err) | ||||
| 	} | ||||
| 
 | ||||
| 	if _, err = w.Write(jsonBytes); err != nil { | ||||
| 		log.Print(err) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| type requestData struct { | ||||
| 	Csr        string `json:"csr"` | ||||
| 	CommonName string `json:"commonName"` | ||||
| } | ||||
| 
 | ||||
| type responseData struct { | ||||
| 	Certificate string   `json:"certificate"` | ||||
| 	CAChain     []string `json:"ca_chain"` | ||||
| } | ||||
| 
 | ||||
| func (h *CertificateSigningHandler) getCAChain(certificate *x509.Certificate) ([]string, error) { | ||||
| 	issuerString := string(certificate.RawIssuer) | ||||
| 
 | ||||
| 	if value, exists := h.caChainMap[issuerString]; exists { | ||||
| 		return value, nil | ||||
| 	} | ||||
| 
 | ||||
| 	result := make([]string, 0) | ||||
| 
 | ||||
| 	appendCert := func(cert *x509.Certificate) { | ||||
| 		result = append( | ||||
| 			result, | ||||
| 			string(pem.EncodeToMemory(&pem.Block{Bytes: cert.Raw, Type: "CERTIFICATE"}))) | ||||
| 		log.Debugf("added %s to cachain", result[len(result)-1]) | ||||
| 	} | ||||
| 
 | ||||
| 	var previous *x509.Certificate | ||||
| 	var count = 0 | ||||
| 	for { | ||||
| 		if len(h.caCertificates) == 0 { | ||||
| 			return nil, errors.New("no CA certificates loaded") | ||||
| 		} | ||||
| 		if count > len(h.caCertificates) { | ||||
| 			return nil, errors.New("could not construct certificate chain") | ||||
| 		} | ||||
| 		for _, caCert := range h.caCertificates { | ||||
| 			if previous == nil { | ||||
| 				if bytes.Equal(caCert.RawSubject, certificate.RawIssuer) { | ||||
| 					previous = caCert | ||||
| 					appendCert(caCert) | ||||
| 				} | ||||
| 			} else if bytes.Equal(previous.RawSubject, previous.RawIssuer) { | ||||
| 				h.caChainMap[issuerString] = result | ||||
| 				return result, nil | ||||
| 			} else if bytes.Equal(caCert.RawSubject, previous.RawIssuer) { | ||||
| 				previous = caCert | ||||
| 				appendCert(caCert) | ||||
| 			} else { | ||||
| 				log.Debugf("skipped certificate %s", caCert.Subject) | ||||
| 			} | ||||
| 		} | ||||
| 		count++ | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										394
									
								
								main.go
									
										
									
									
									
								
							
							
						
						
									
										394
									
								
								main.go
									
										
									
									
									
								
							|  | @ -1,19 +1,15 @@ | |||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"crypto/rand" | ||||
| 	"crypto/tls" | ||||
| 	"crypto/x509" | ||||
| 	"encoding/base64" | ||||
| 	"encoding/json" | ||||
| 	"encoding/pem" | ||||
| 	"fmt" | ||||
| 	"html/template" | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
| 	"os" | ||||
| 	"os/exec" | ||||
| 	"os/signal" | ||||
| 	"strings" | ||||
| 	"syscall" | ||||
|  | @ -24,346 +20,33 @@ import ( | |||
| 	"github.com/nicksnyder/go-i18n/v2/i18n" | ||||
| 	log "github.com/sirupsen/logrus" | ||||
| 	"golang.org/x/text/language" | ||||
| 
 | ||||
| 	"git.dittberner.info/jan/browser_csr_generation/handlers" | ||||
| ) | ||||
| 
 | ||||
| type signCertificate struct{} | ||||
| 
 | ||||
| type requestData struct { | ||||
| 	Csr        string `json:"csr"` | ||||
| 	CommonName string `json:"commonName"` | ||||
| } | ||||
| 
 | ||||
| type responseData struct { | ||||
| 	Certificate string   `json:"certificate"` | ||||
| 	CAChain     []string `json:"ca_chain"` | ||||
| } | ||||
| 
 | ||||
| var caCertificates []*x509.Certificate | ||||
| 
 | ||||
| func (h *signCertificate) sign(csrPem string, commonName string) (certPem string, caChain []string, err error) { | ||||
| 	log.Printf("received CSR for %s:\n\n%s", commonName, csrPem) | ||||
| 	subjectDN := fmt.Sprintf("/CN=%s", commonName) | ||||
| 	var csrFile *os.File | ||||
| 	if csrFile, err = ioutil.TempFile("", "*.csr.pem"); err != nil { | ||||
| 		log.Errorf("could not open temporary file: %s", err) | ||||
| 		return | ||||
| 	} | ||||
| 	if _, err = csrFile.Write([]byte(csrPem)); err != nil { | ||||
| 		log.Errorf("could not write CSR to file: %s", err) | ||||
| 		return | ||||
| 	} | ||||
| 	if err = csrFile.Close(); err != nil { | ||||
| 		log.Errorf("could not close CSR file: %s", err) | ||||
| 		return | ||||
| 	} | ||||
| 	defer func(file *os.File) { | ||||
| 		err = os.Remove(file.Name()) | ||||
| 		if err != nil { | ||||
| 			log.Errorf("could not remove temporary file: %s", err) | ||||
| 		} | ||||
| 	}(csrFile) | ||||
| 
 | ||||
| 	opensslCommand := exec.Command( | ||||
| 		"openssl", "ca", "-config", "ca.cnf", | ||||
| 		"-policy", "policy_match", "-extensions", "client_ext", | ||||
| 		"-batch", "-subj", subjectDN, "-utf8", "-rand_serial", "-in", "in.pem") | ||||
| 	var out, cmdErr bytes.Buffer | ||||
| 	opensslCommand.Stdout = &out | ||||
| 	opensslCommand.Stderr = &cmdErr | ||||
| 	err = opensslCommand.Run() | ||||
| 	if err != nil { | ||||
| 		log.Print(err) | ||||
| 		log.Print(cmdErr.String()) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	var block *pem.Block | ||||
| 	if block, _ = pem.Decode(out.Bytes()); block == nil { | ||||
| 		err = fmt.Errorf("could not decode pem") | ||||
| 		return | ||||
| 	} | ||||
| 	var certificate *x509.Certificate | ||||
| 	if certificate, err = x509.ParseCertificate(block.Bytes); err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	certPem = string(pem.EncodeToMemory(&pem.Block{ | ||||
| 		Type:  "CERTIFICATE", | ||||
| 		Bytes: certificate.Raw, | ||||
| 	})) | ||||
| 
 | ||||
| 	caChain, err = h.getCAChain(certificate) | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
| func (h *signCertificate) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||||
| 	if r.Method != "POST" { | ||||
| 		http.Error(w, "Only POST requests support", http.StatusMethodNotAllowed) | ||||
| 		return | ||||
| 	} | ||||
| 	if r.Header.Get("content-type") != "application/json" { | ||||
| 		http.Error(w, "Only JSON content is accepted", http.StatusNotAcceptable) | ||||
| 		return | ||||
| 	} | ||||
| 	var err error | ||||
| 	var requestBody requestData | ||||
| 
 | ||||
| 	var responseData responseData | ||||
| 
 | ||||
| 	if err = json.NewDecoder(r.Body).Decode(&requestBody); err != nil { | ||||
| 		log.Print(err) | ||||
| 		http.Error(w, err.Error(), http.StatusBadRequest) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	responseData.Certificate, responseData.CAChain, err = h.sign(requestBody.Csr, requestBody.CommonName) | ||||
| 	if err != nil { | ||||
| 		http.Error(w, "Could not sign certificate", http.StatusInternalServerError) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	var jsonBytes []byte | ||||
| 	if jsonBytes, err = json.Marshal(&responseData); err != nil { | ||||
| 		log.Print(err) | ||||
| 	} | ||||
| 
 | ||||
| 	if _, err = w.Write(jsonBytes); err != nil { | ||||
| 		log.Print(err) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (*signCertificate) getCAChain(certificate *x509.Certificate) ([]string, error) { | ||||
| 	result := make([]string, 0) | ||||
| 
 | ||||
| 	appendCert := func(cert *x509.Certificate) { | ||||
| 		result = append( | ||||
| 			result, | ||||
| 			string(pem.EncodeToMemory(&pem.Block{Bytes: cert.Raw, Type: "CERTIFICATE"}))) | ||||
| 		log.Debugf("added %s to cachain", result[len(result)-1]) | ||||
| 	} | ||||
| 
 | ||||
| 	var previous *x509.Certificate | ||||
| 	for { | ||||
| 		if len(caCertificates) == 0 { | ||||
| 			return result, nil | ||||
| 		} | ||||
| 		for _, caCert := range caCertificates { | ||||
| 			if previous == nil { | ||||
| 				if bytes.Equal(caCert.RawSubject, certificate.RawIssuer) { | ||||
| 					previous = caCert | ||||
| 					appendCert(caCert) | ||||
| 				} | ||||
| 			} else if bytes.Equal(previous.RawSubject, previous.RawIssuer) { | ||||
| 				return result, nil | ||||
| 			} else if bytes.Equal(caCert.RawSubject, previous.RawIssuer) { | ||||
| 				previous = caCert | ||||
| 				appendCert(caCert) | ||||
| 			} else { | ||||
| 				log.Debugf("skipped certificate %s", caCert.Subject) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| type indexHandler struct { | ||||
| 	Bundle *i18n.Bundle | ||||
| } | ||||
| 
 | ||||
| func (i *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||||
| 	localizer := i18n.NewLocalizer(i.Bundle, r.Header.Get("Accept-Language")) | ||||
| 	csrGenTitle := localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID:    "CSRGenTitle", | ||||
| 		Other: "CSR generation in browser", | ||||
| 	}}) | ||||
| 	nameLabel := localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID:    "NameLabel", | ||||
| 		Other: "Your name", | ||||
| 	}}) | ||||
| 	nameHelpText := localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID:    "NameHelpText", | ||||
| 		Other: "Please input your name as it should be added to your certificate", | ||||
| 	}}) | ||||
| 	passwordLabel := localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID:    "PasswordLabel", | ||||
| 		Other: "Password for your client certificate", | ||||
| 	}}) | ||||
| 	rsaKeySizeLegend := localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID:    "RSAKeySizeLabel", | ||||
| 		Other: "RSA Key Size", | ||||
| 	}}) | ||||
| 	rsa3072Label := localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID:    "RSA3072Label", | ||||
| 		Other: "3072 Bit", | ||||
| 	}}) | ||||
| 	rsa2048Label := localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID:    "RSA2048Label", | ||||
| 		Other: "2048 Bit (not recommended)", | ||||
| 	}}) | ||||
| 	rsa4096Label := localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID:    "RSA4096Label", | ||||
| 		Other: "4096 Bit", | ||||
| 	}}) | ||||
| 	rsaHelpText := localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID: "RSAHelpText", | ||||
| 		Other: "An RSA key pair will be generated in your browser. Longer key" + | ||||
| 			" sizes provide better security but take longer to generate.", | ||||
| 	}}) | ||||
| 	csrButtonLabel := localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID:    "CSRButtonLabel", | ||||
| 		Other: "Generate signing request", | ||||
| 	}}) | ||||
| 	statusLoading := localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID:    "StatusLoading", | ||||
| 		Other: "Loading ...", | ||||
| 	}}) | ||||
| 	downloadLabel := localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID:    "DownloadLabel", | ||||
| 		Other: "Download", | ||||
| 	}}) | ||||
| 	downloadDescription := localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID: "DownloadDescription", | ||||
| 		Other: "Your key material is ready for download. The downloadable file contains your private key and your" + | ||||
| 			" certificate encrypted with your password. You can now use the file to install your certificate in your" + | ||||
| 			" browser or other applications.", | ||||
| 	}}) | ||||
| 
 | ||||
| 	t := template.Must(template.ParseFiles("templates/index.html")) | ||||
| 	err := t.Execute(w, map[string]interface{}{ | ||||
| 		"Title":               csrGenTitle, | ||||
| 		"NameLabel":           nameLabel, | ||||
| 		"NameHelpText":        nameHelpText, | ||||
| 		"PasswordLabel":       passwordLabel, | ||||
| 		"RSAKeySizeLegend":    rsaKeySizeLegend, | ||||
| 		"RSA3072Label":        rsa3072Label, | ||||
| 		"RSA2048Label":        rsa2048Label, | ||||
| 		"RSA4096Label":        rsa4096Label, | ||||
| 		"RSAHelpText":         rsaHelpText, | ||||
| 		"CSRButtonLabel":      csrButtonLabel, | ||||
| 		"StatusLoading":       statusLoading, | ||||
| 		"DownloadDescription": downloadDescription, | ||||
| 		"DownloadLabel":       downloadLabel, | ||||
| 		csrf.TemplateTag:      csrf.TemplateField(r), | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		log.Panic(err) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| type jsLocalesHandler struct { | ||||
| 	Bundle *i18n.Bundle | ||||
| } | ||||
| 
 | ||||
| func (j *jsLocalesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||||
| 	parts := strings.Split(r.URL.Path, "/") | ||||
| 	if len(parts) != 4 { | ||||
| 		http.Error(w, "Not found", http.StatusNotFound) | ||||
| 		return | ||||
| 	} | ||||
| 	lang := parts[2] | ||||
| 
 | ||||
| 	localizer := i18n.NewLocalizer(j.Bundle, lang) | ||||
| 
 | ||||
| 	type translationData struct { | ||||
| 		Keygen struct { | ||||
| 			Started   string `json:"started"` | ||||
| 			Running   string `json:"running"` | ||||
| 			Generated string `json:"generated"` | ||||
| 		} `json:"keygen"` | ||||
| 	} | ||||
| 
 | ||||
| 	translations := &translationData{} | ||||
| 	translations.Keygen.Started = localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID:    "JavaScript.KeyGen.Started", | ||||
| 		Other: "started key generation", | ||||
| 	}}) | ||||
| 	translations.Keygen.Running = localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID:    "JavaScript.KeyGen.Running", | ||||
| 		Other: "key generation running for __seconds__ seconds", | ||||
| 	}}) | ||||
| 	translations.Keygen.Generated = localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID:    "JavaScript.KeyGen.Generated", | ||||
| 		Other: "key generated in __seconds__ seconds", | ||||
| 	}}) | ||||
| 
 | ||||
| 	encoder := json.NewEncoder(w) | ||||
| 	if err := encoder.Encode(translations); err != nil { | ||||
| 		http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func generateRandomBytes(count int) []byte { | ||||
| 	randomBytes := make([]byte, count) | ||||
| 
 | ||||
| 	_, err := rand.Read(randomBytes) | ||||
| 	if err != nil { | ||||
| 		log.Fatalf("could not read random bytes: %v", err) | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	return randomBytes | ||||
| } | ||||
| 
 | ||||
| func init() { | ||||
| 	var err error | ||||
| 	caCertificates = make([]*x509.Certificate, 2) | ||||
| 	for index, certFile := range []string{"example_ca/sub/ca.crt.pem", "example_ca/root/ca.crt.pem"} { | ||||
| 		var certBytes []byte | ||||
| 		if certBytes, err = ioutil.ReadFile(certFile); err != nil { | ||||
| 			log.Panic(err) | ||||
| 		} | ||||
| 		var block *pem.Block | ||||
| 		if block, _ = pem.Decode(certBytes); block == nil { | ||||
| 			log.Panicf("no PEM data found in %s", certFile) | ||||
| 			return | ||||
| 		} | ||||
| 		if caCertificates[index], err = x509.ParseCertificate(block.Bytes); err != nil { | ||||
| 			log.Panic(err) | ||||
| 		} | ||||
| 	} | ||||
| 	log.Infof("read %d CA certificates", len(caCertificates)) | ||||
| } | ||||
| 
 | ||||
| func main() { | ||||
| 	tlsConfig := &tls.Config{ | ||||
| 		CipherSuites: []uint16{ | ||||
| 			tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, | ||||
| 			tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, | ||||
| 			tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, | ||||
| 		}, | ||||
| 		NextProtos:               []string{"h2"}, | ||||
| 		PreferServerCipherSuites: true, | ||||
| 		MinVersion:               tls.VersionTLS12, | ||||
| 	} | ||||
| 
 | ||||
| 	bundle := i18n.NewBundle(language.English) | ||||
| 	bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal) | ||||
| 	for _, lang := range []string{"en-US", "de-DE"} { | ||||
| 		if _, err := bundle.LoadMessageFile(fmt.Sprintf("active.%s.toml", lang)); err != nil { | ||||
| 			log.Panic(err) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	bundle := loadI18nBundle() | ||||
| 	mux := http.NewServeMux() | ||||
| 
 | ||||
| 	var csrfKey []byte = nil | ||||
| 	csrfKey := initCSRFKey() | ||||
| 
 | ||||
| 	if csrfB64, exists := os.LookupEnv("CSRF_KEY"); exists { | ||||
| 		csrfKey, _ = base64.RawStdEncoding.DecodeString(csrfB64) | ||||
| 		log.Info("read CSRF key from environment variable") | ||||
| 	} | ||||
| 	if csrfKey == nil { | ||||
| 		csrfKey = generateRandomBytes(32) | ||||
| 		log.Infof( | ||||
| 			"generated new random CSRF key, set environment variable CSRF_KEY to %s to "+ | ||||
| 				"keep the same key for new sessions", | ||||
| 			base64.RawStdEncoding.EncodeToString(csrfKey)) | ||||
| 	} | ||||
| 
 | ||||
| 	mux.Handle("/sign/", &signCertificate{}) | ||||
| 	mux.Handle("/", &indexHandler{Bundle: bundle}) | ||||
| 	mux.Handle("/sign/", handlers.NewCertificateSigningHandler(loadCACertificates())) | ||||
| 	mux.Handle("/", handlers.NewIndexHandler(bundle)) | ||||
| 	fileServer := http.FileServer(http.Dir("./public")) | ||||
| 	mux.Handle("/css/", fileServer) | ||||
| 	mux.Handle("/js/", fileServer) | ||||
| 	mux.Handle("/locales/", &jsLocalesHandler{Bundle: bundle}) | ||||
| 	mux.Handle("/locales/", handlers.NewJSLocalesHandler(bundle)) | ||||
| 	server := http.Server{ | ||||
| 		Addr:              ":8000", | ||||
| 		Handler:           csrf.Protect(csrfKey, csrf.FieldName("csrfToken"), csrf.RequestHeader("X-CSRF-Token"))(mux), | ||||
|  | @ -392,3 +75,64 @@ func main() { | |||
| 	log.Infof("received %s, shutting down", s) | ||||
| 	_ = server.Close() | ||||
| } | ||||
| 
 | ||||
| func loadI18nBundle() *i18n.Bundle { | ||||
| 	bundle := i18n.NewBundle(language.English) | ||||
| 	bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal) | ||||
| 	for _, lang := range []string{"en-US", "de-DE"} { | ||||
| 		if _, err := bundle.LoadMessageFile(fmt.Sprintf("active.%s.toml", lang)); err != nil { | ||||
| 			log.Panic(err) | ||||
| 		} | ||||
| 	} | ||||
| 	return bundle | ||||
| } | ||||
| 
 | ||||
| func initCSRFKey() []byte { | ||||
| 	var csrfKey []byte = nil | ||||
| 
 | ||||
| 	if csrfB64, exists := os.LookupEnv("CSRF_KEY"); exists { | ||||
| 		csrfKey, _ = base64.RawStdEncoding.DecodeString(csrfB64) | ||||
| 		log.Info("read CSRF key from environment variable") | ||||
| 	} | ||||
| 	if csrfKey == nil { | ||||
| 		csrfKey = generateRandomBytes(32) | ||||
| 		log.Infof( | ||||
| 			"generated new random CSRF key, set environment variable CSRF_KEY to %s to "+ | ||||
| 				"keep the same key for new sessions", | ||||
| 			base64.RawStdEncoding.EncodeToString(csrfKey)) | ||||
| 	} | ||||
| 	return csrfKey | ||||
| } | ||||
| 
 | ||||
| func generateRandomBytes(count int) []byte { | ||||
| 	randomBytes := make([]byte, count) | ||||
| 
 | ||||
| 	_, err := rand.Read(randomBytes) | ||||
| 	if err != nil { | ||||
| 		log.Fatalf("could not read random bytes: %v", err) | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	return randomBytes | ||||
| } | ||||
| 
 | ||||
| func loadCACertificates() (caCertificates []*x509.Certificate) { | ||||
| 	var err error | ||||
| 	caCertificates = make([]*x509.Certificate, 2) | ||||
| 	for index, certFile := range []string{"example_ca/sub/ca.crt.pem", "example_ca/root/ca.crt.pem"} { | ||||
| 		var certBytes []byte | ||||
| 		if certBytes, err = ioutil.ReadFile(certFile); err != nil { | ||||
| 			log.Panic(err) | ||||
| 		} | ||||
| 		var block *pem.Block | ||||
| 		if block, _ = pem.Decode(certBytes); block == nil { | ||||
| 			log.Panicf("no PEM data found in %s", certFile) | ||||
| 			return | ||||
| 		} | ||||
| 		if caCertificates[index], err = x509.ParseCertificate(block.Bytes); err != nil { | ||||
| 			log.Panic(err) | ||||
| 		} | ||||
| 	} | ||||
| 	log.Infof("read %d CA certificates", len(caCertificates)) | ||||
| 	return | ||||
| } | ||||
|  |  | |||
		Reference in a new issue