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 | package main | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"bytes" |  | ||||||
| 	"crypto/rand" | 	"crypto/rand" | ||||||
| 	"crypto/tls" | 	"crypto/tls" | ||||||
| 	"crypto/x509" | 	"crypto/x509" | ||||||
| 	"encoding/base64" | 	"encoding/base64" | ||||||
| 	"encoding/json" |  | ||||||
| 	"encoding/pem" | 	"encoding/pem" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"html/template" |  | ||||||
| 	"io/ioutil" | 	"io/ioutil" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"os" | 	"os" | ||||||
| 	"os/exec" |  | ||||||
| 	"os/signal" | 	"os/signal" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"syscall" | 	"syscall" | ||||||
|  | @ -24,346 +20,33 @@ import ( | ||||||
| 	"github.com/nicksnyder/go-i18n/v2/i18n" | 	"github.com/nicksnyder/go-i18n/v2/i18n" | ||||||
| 	log "github.com/sirupsen/logrus" | 	log "github.com/sirupsen/logrus" | ||||||
| 	"golang.org/x/text/language" | 	"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() { | func main() { | ||||||
| 	tlsConfig := &tls.Config{ | 	tlsConfig := &tls.Config{ | ||||||
| 		CipherSuites: []uint16{ | 		CipherSuites: []uint16{ | ||||||
| 			tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, | 			tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, | ||||||
| 			tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, | 			tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, | ||||||
|  | 			tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, | ||||||
| 		}, | 		}, | ||||||
| 		NextProtos:               []string{"h2"}, | 		NextProtos:               []string{"h2"}, | ||||||
| 		PreferServerCipherSuites: true, | 		PreferServerCipherSuites: true, | ||||||
| 		MinVersion:               tls.VersionTLS12, | 		MinVersion:               tls.VersionTLS12, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	bundle := i18n.NewBundle(language.English) | 	bundle := loadI18nBundle() | ||||||
| 	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) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	mux := http.NewServeMux() | 	mux := http.NewServeMux() | ||||||
| 
 | 
 | ||||||
| 	var csrfKey []byte = nil | 	csrfKey := initCSRFKey() | ||||||
| 
 | 
 | ||||||
| 	if csrfB64, exists := os.LookupEnv("CSRF_KEY"); exists { | 	mux.Handle("/sign/", handlers.NewCertificateSigningHandler(loadCACertificates())) | ||||||
| 		csrfKey, _ = base64.RawStdEncoding.DecodeString(csrfB64) | 	mux.Handle("/", handlers.NewIndexHandler(bundle)) | ||||||
| 		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}) |  | ||||||
| 	fileServer := http.FileServer(http.Dir("./public")) | 	fileServer := http.FileServer(http.Dir("./public")) | ||||||
| 	mux.Handle("/css/", fileServer) | 	mux.Handle("/css/", fileServer) | ||||||
| 	mux.Handle("/js/", fileServer) | 	mux.Handle("/js/", fileServer) | ||||||
| 	mux.Handle("/locales/", &jsLocalesHandler{Bundle: bundle}) | 	mux.Handle("/locales/", handlers.NewJSLocalesHandler(bundle)) | ||||||
| 	server := http.Server{ | 	server := http.Server{ | ||||||
| 		Addr:              ":8000", | 		Addr:              ":8000", | ||||||
| 		Handler:           csrf.Protect(csrfKey, csrf.FieldName("csrfToken"), csrf.RequestHeader("X-CSRF-Token"))(mux), | 		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) | 	log.Infof("received %s, shutting down", s) | ||||||
| 	_ = server.Close() | 	_ = 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