Implement i18n support
This commit adds internationalization support and a german translation.
This commit is contained in:
		
							parent
							
								
									3503e09212
								
							
						
					
					
						commit
						e13c9d174b
					
				
					 13 changed files with 541 additions and 5912 deletions
				
			
		
							
								
								
									
										1
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							|  | @ -1,5 +1,6 @@ | |||
| *.pem | ||||
| .*.swp | ||||
| /translate.*.toml | ||||
| /.idea/ | ||||
| /exampleca/ | ||||
| /node_modules/ | ||||
|  |  | |||
							
								
								
									
										54
									
								
								README.md
									
										
									
									
									
								
							
							
						
						
									
										54
									
								
								README.md
									
										
									
									
									
								
							|  | @ -1,11 +1,9 @@ | |||
| # Browser PKCS#10 CSR generation PoC | ||||
| 
 | ||||
| This repository contains a small proof of concept implementation of browser | ||||
| based PKCS#10 certificate signing request and PKCS#12 key store generation | ||||
| using [node-forge](https://github.com/digitalbazaar/forge). | ||||
| This repository contains a small proof of concept implementation of browser based PKCS#10 certificate signing request | ||||
| and PKCS#12 key store generation using [node-forge](https://github.com/digitalbazaar/forge). | ||||
| 
 | ||||
| The backend is implemented in [Go](https://golang.org/) and utilizes openssl | ||||
| for the signing operations. | ||||
| The backend is implemented in [Go](https://golang.org/) and utilizes openssl for the signing operations. | ||||
| 
 | ||||
| ## Running | ||||
| 
 | ||||
|  | @ -38,14 +36,52 @@ for the signing operations. | |||
|     go run main.go | ||||
|     ``` | ||||
| 
 | ||||
|     Open https://localhost:8000/ in your browser. | ||||
|    Open https://localhost:8000/ in your browser. | ||||
| 
 | ||||
| 4. Run gulp watch | ||||
| 
 | ||||
|     You can run a [gulp watch](https://gulpjs.com/docs/en/getting-started/watching-files/) | ||||
|     in a second terminal window to automatically publish changes to the files | ||||
|     in the `src` directory: | ||||
|    You can run a [gulp watch](https://gulpjs.com/docs/en/getting-started/watching-files/) | ||||
|    in a second terminal window to automatically publish changes to the files in the `src` directory: | ||||
| 
 | ||||
|     ``` | ||||
|     gulp watch | ||||
|     ``` | ||||
| 
 | ||||
| ## Translations | ||||
| 
 | ||||
| This PoC uses [go-i18n](https://github.com/nicksnyder/go-i18n/) for internationalization (i18n) support. | ||||
| 
 | ||||
| The translation workflow needs the `go18n` binary which can be installed via | ||||
| 
 | ||||
| ``` | ||||
| go get -u  github.com/nicksnyder/go-i18n/v2/goi18n | ||||
| ``` | ||||
| 
 | ||||
| To extract new messages from the code run | ||||
| 
 | ||||
| ``` | ||||
| goi18n extract | ||||
| ``` | ||||
| 
 | ||||
| Then use | ||||
| 
 | ||||
| ``` | ||||
| goi18n merge active.*.toml | ||||
| ``` | ||||
| 
 | ||||
| to create TOML files for translation as `translate.<locale>.toml`. After translating the messages run | ||||
| 
 | ||||
| ``` | ||||
| goi18n merge active.*.toml translate.*.toml | ||||
| ``` | ||||
| 
 | ||||
| to merge the messages back into the active translation files. To add a new language you need to add the language code | ||||
| to `main.go`'s i18n bundle loading code | ||||
| 
 | ||||
| ``` | ||||
| for _, lang := range []string{"en-US", "de-DE"} { | ||||
|     if _, err := bundle.LoadMessageFile(fmt.Sprintf("active.%s.toml", lang)); err != nil { | ||||
|         log.Panic(err) | ||||
|     } | ||||
| } | ||||
| ``` | ||||
							
								
								
									
										59
									
								
								active.de-DE.toml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								active.de-DE.toml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,59 @@ | |||
| [CSRButtonLabel] | ||||
| hash = "sha1-7f7bcb57602a96a49c8df4868fad7b81992e0734" | ||||
| other = "Zertifikats-Signier-Anfrage erzeugen" | ||||
| 
 | ||||
| [CSRGenTitle] | ||||
| hash = "sha1-f1a8f21b12fe51250da4a11f1c6ab28eab69b69d" | ||||
| other = "CSR-Erzeugung im Browser" | ||||
| 
 | ||||
| ["JavaScript.KeyGen.Generated"] | ||||
| hash = "sha1-34cdfcdc837e3fc052733a3588cc3923b793103e" | ||||
| other = "Schlüssel in __seconds__ Sekunden erzeugt" | ||||
| 
 | ||||
| ["JavaScript.KeyGen.Running"] | ||||
| hash = "sha1-fd272f37118fe10395f6238a3eae94685b5f8cf1" | ||||
| other = "Schlüsselerzeugung läuft seit  __seconds__ Sekunden" | ||||
| 
 | ||||
| ["JavaScript.KeyGen.Started"] | ||||
| hash = "sha1-e68739d705d5eb16317984a95a486fb9ff9bae6d" | ||||
| other = "Schlüsselerzeugung gestartet" | ||||
| 
 | ||||
| [NameHelpText] | ||||
| hash = "sha1-52b81217549e37d161090a04b7f84223a270928e" | ||||
| other = "Gib deinen Namen so ein, wie er im Zertifikat erscheinen soll" | ||||
| 
 | ||||
| [NameLabel] | ||||
| hash = "sha1-ab42293e29e1ffb306c1403dd95144d664853a60" | ||||
| other = "Dein Name" | ||||
| 
 | ||||
| [PasswordLabel] | ||||
| hash = "sha1-2b5e8edbf45819afdfa973c224b6b02d699e60de" | ||||
| other = "Passwort für dein Client-Zertifikat" | ||||
| 
 | ||||
| [RSA2048Label] | ||||
| hash = "sha1-2b1e7b638c31426d30d7e4bdebadbaa07d7521b0" | ||||
| other = "2048 Bit (nicht empfohlen)" | ||||
| 
 | ||||
| [RSA3072Label] | ||||
| hash = "sha1-97d1a8f9e8c5cf1f473b4a6fa0b5c39905f0f747" | ||||
| other = "3072 Bit" | ||||
| 
 | ||||
| [RSA4096Label] | ||||
| hash = "sha1-b14d7490195ac7f2d649f3b75dd2fe0daea53967" | ||||
| other = "4096 Bit" | ||||
| 
 | ||||
| [RSAHelpText] | ||||
| hash = "sha1-82511ecf2909ba189d7b16a828fce97c9359fad1" | ||||
| other = "In Deinem Browser wird ein RSA-Schlüsselpaar erzeugt. Größere Schlüssellängen bieten eine erhöhte Sicherheit, benötigen aber bei der Erzeugung mehr Zeit." | ||||
| 
 | ||||
| [RSAKeySizeLabel] | ||||
| hash = "sha1-bd446df78ad62000d6516a95594a24b98688e1fa" | ||||
| other = "RSA-Schlüssellänge" | ||||
| 
 | ||||
| [SendCSRButtonLabel] | ||||
| hash = "sha1-376b8bd1617b2c9d54272604677b1d75d3e6f477" | ||||
| other = "Signieranfrage abschicken" | ||||
| 
 | ||||
| [StatusLoading] | ||||
| hash = "sha1-530afa5bce434b05e3a10e83ff2567f7f8622af9" | ||||
| other = "Lade ..." | ||||
							
								
								
									
										59
									
								
								active.en-US.toml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								active.en-US.toml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,59 @@ | |||
| [CSRButtonLabel] | ||||
| hash = "sha1-7f7bcb57602a96a49c8df4868fad7b81992e0734" | ||||
| other = "Generate signing request" | ||||
| 
 | ||||
| [CSRGenTitle] | ||||
| hash = "sha1-f1a8f21b12fe51250da4a11f1c6ab28eab69b69d" | ||||
| other = "CSR generation in browser" | ||||
| 
 | ||||
| ["JavaScript.KeyGen.Generated"] | ||||
| hash = "sha1-34cdfcdc837e3fc052733a3588cc3923b793103e" | ||||
| other = "key generated in __seconds__ seconds" | ||||
| 
 | ||||
| ["JavaScript.KeyGen.Running"] | ||||
| hash = "sha1-fd272f37118fe10395f6238a3eae94685b5f8cf1" | ||||
| other = "key generation running for __seconds__ seconds" | ||||
| 
 | ||||
| ["JavaScript.KeyGen.Started"] | ||||
| hash = "sha1-e68739d705d5eb16317984a95a486fb9ff9bae6d" | ||||
| other = "started key generation" | ||||
| 
 | ||||
| [NameHelpText] | ||||
| hash = "sha1-52b81217549e37d161090a04b7f84223a270928e" | ||||
| other = "Please input your name as it should be added to your certificate" | ||||
| 
 | ||||
| [NameLabel] | ||||
| hash = "sha1-ab42293e29e1ffb306c1403dd95144d664853a60" | ||||
| other = "Your name" | ||||
| 
 | ||||
| [PasswordLabel] | ||||
| hash = "sha1-2b5e8edbf45819afdfa973c224b6b02d699e60de" | ||||
| other = "Password for your client certificate" | ||||
| 
 | ||||
| [RSA2048Label] | ||||
| hash = "sha1-2b1e7b638c31426d30d7e4bdebadbaa07d7521b0" | ||||
| other = "2048 Bit (not recommended)" | ||||
| 
 | ||||
| [RSA3072Label] | ||||
| hash = "sha1-97d1a8f9e8c5cf1f473b4a6fa0b5c39905f0f747" | ||||
| other = "3072 Bit" | ||||
| 
 | ||||
| [RSA4096Label] | ||||
| hash = "sha1-b14d7490195ac7f2d649f3b75dd2fe0daea53967" | ||||
| other = "4096 Bit" | ||||
| 
 | ||||
| [RSAHelpText] | ||||
| hash = "sha1-82511ecf2909ba189d7b16a828fce97c9359fad1" | ||||
| other = "An RSA key pair will be generated in your browser. Longer key sizes provide better security but take longer to generate." | ||||
| 
 | ||||
| [RSAKeySizeLabel] | ||||
| hash = "sha1-bd446df78ad62000d6516a95594a24b98688e1fa" | ||||
| other = "RSA Key Size" | ||||
| 
 | ||||
| [SendCSRButtonLabel] | ||||
| hash = "sha1-376b8bd1617b2c9d54272604677b1d75d3e6f477" | ||||
| other = "Send signing request" | ||||
| 
 | ||||
| [StatusLoading] | ||||
| hash = "sha1-530afa5bce434b05e3a10e83ff2567f7f8622af9" | ||||
| other = "Loading ..." | ||||
							
								
								
									
										15
									
								
								active.en.toml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								active.en.toml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,15 @@ | |||
| CSRButtonLabel = "Generate signing request" | ||||
| CSRGenTitle = "CSR generation in browser" | ||||
| "JavaScript.KeyGen.Generated" = "key generated in __seconds__ seconds" | ||||
| "JavaScript.KeyGen.Running" = "key generation running for __seconds__ seconds" | ||||
| "JavaScript.KeyGen.Started" = "started key generation" | ||||
| NameHelpText = "Please input your name as it should be added to your certificate" | ||||
| NameLabel = "Your name" | ||||
| PasswordLabel = "Password for your client certificate" | ||||
| RSA2048Label = "2048 Bit (not recommended)" | ||||
| RSA3072Label = "3072 Bit" | ||||
| RSA4096Label = "4096 Bit" | ||||
| RSAHelpText = "An RSA key pair will be generated in your browser. Longer key sizes provide better security but take longer to generate." | ||||
| RSAKeySizeLabel = "RSA Key Size" | ||||
| SendCSRButtonLabel = "Send signing request" | ||||
| StatusLoading = "Loading ..." | ||||
							
								
								
									
										7
									
								
								go.mod
									
										
									
									
									
								
							
							
						
						
									
										7
									
								
								go.mod
									
										
									
									
									
								
							|  | @ -1,3 +1,10 @@ | |||
| module git.dittberner.info/jan/browser_csr_generation | ||||
| 
 | ||||
| go 1.13 | ||||
| 
 | ||||
| require ( | ||||
| 	github.com/BurntSushi/toml v0.3.1 | ||||
| 	github.com/nicksnyder/go-i18n/v2 v2.1.1 | ||||
| 	golang.org/x/text v0.3.4 | ||||
| 	gopkg.in/yaml.v2 v2.4.0 // indirect | ||||
| ) | ||||
|  |  | |||
							
								
								
									
										14
									
								
								go.sum
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								go.sum
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,14 @@ | |||
| github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= | ||||
| github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= | ||||
| github.com/nicksnyder/go-i18n/v2 v2.1.1 h1:ATCOanRDlrfKVB4WHAdJnLEqZtDmKYsweqsOUYflnBU= | ||||
| github.com/nicksnyder/go-i18n/v2 v2.1.1/go.mod h1:d++QJC9ZVf7pa48qrsRWhMJ5pSHIPmS3OLqK1niyLxs= | ||||
| golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||||
| golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= | ||||
| golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||||
| golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | ||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= | ||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||
| gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= | ||||
| gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||||
| gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= | ||||
| gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= | ||||
|  | @ -42,7 +42,8 @@ function publishAssets() { | |||
|         'node_modules/popper.js/dist/*.map', | ||||
|         'node_modules/jquery/dist/*.*', | ||||
|         'node_modules/bootstrap/dist/js/*.*', | ||||
|         'node_modules/node-forge/dist/*.*' | ||||
|         'node_modules/node-forge/dist/*.*', | ||||
|         'node_modules/i18next-client/i18next.min.js', | ||||
|     ]).pipe(dest('public/js')); | ||||
| } | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										139
									
								
								main.go
									
										
									
									
									
								
							
							
						
						
									
										139
									
								
								main.go
									
										
									
									
									
								
							|  | @ -5,11 +5,17 @@ import ( | |||
| 	"crypto/tls" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"html/template" | ||||
| 	"io/ioutil" | ||||
| 	"log" | ||||
| 	"net/http" | ||||
| 	"os/exec" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/BurntSushi/toml" | ||||
| 	"github.com/nicksnyder/go-i18n/v2/i18n" | ||||
| 	"golang.org/x/text/language" | ||||
| ) | ||||
| 
 | ||||
| type signCertificate struct{} | ||||
|  | @ -83,6 +89,124 @@ func (h *signCertificate) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |||
| 	} | ||||
| } | ||||
| 
 | ||||
| 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 ...", | ||||
| 	}}) | ||||
| 	sendCSRButtonLabel := localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID:    "SendCSRButtonLabel", | ||||
| 		Other: "Send signing request", | ||||
| 	}}) | ||||
| 
 | ||||
| 	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, | ||||
| 		"SendCSRButtonLabel": sendCSRButtonLabel, | ||||
| 	}) | ||||
| 	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 main() { | ||||
| 	tlsConfig := &tls.Config{ | ||||
| 		CipherSuites: []uint16{ | ||||
|  | @ -93,9 +217,22 @@ func main() { | |||
| 		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) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	mux := http.NewServeMux() | ||||
| 	mux.Handle("/", http.FileServer(http.Dir("public"))) | ||||
| 	mux.Handle("/sign/", &signCertificate{}) | ||||
| 	mux.Handle("/", &indexHandler{Bundle: bundle}) | ||||
| 	fileServer := http.FileServer(http.Dir("./public")) | ||||
| 	mux.Handle("/css/", fileServer) | ||||
| 	mux.Handle("/js/", fileServer) | ||||
| 	mux.Handle("/locales/", &jsLocalesHandler{Bundle: bundle}) | ||||
| 	server := http.Server{ | ||||
| 		Addr:              ":8000", | ||||
| 		Handler:           mux, | ||||
|  |  | |||
							
								
								
									
										5914
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										5914
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							|  | @ -24,8 +24,10 @@ | |||
|     "gulp-sourcemaps": "^3.0.0", | ||||
|     "gulp-sri-hash": "^2.2.1", | ||||
|     "gulp-uglify": "^3.0.2", | ||||
|     "i18next-client": "^1.11.4", | ||||
|     "jquery": "^3.5.1", | ||||
|     "node-forge": "^0.10.0", | ||||
|     "popper.js": "^1.16.1" | ||||
|   } | ||||
|   }, | ||||
|   "dependencies": {} | ||||
| } | ||||
|  |  | |||
|  | @ -69,9 +69,10 @@ | |||
|     <pre id="csr"></pre> | ||||
|     <pre id="crt"></pre> | ||||
| </div> | ||||
| <script src="../public/js/jquery.slim.min.js"></script> | ||||
| <script src="../public/js/jquery.min.js"></script> | ||||
| <script src="../public/js/forge.all.min.js"></script> | ||||
| <script src="../public/js/bootstrap.bundle.min.js"></script> | ||||
| <script src="../public/js/i18next.min.js"></script> | ||||
| <script> | ||||
|     async function postData(url = '', data = {}) { | ||||
|         const response = await fetch(url, { | ||||
|  | @ -166,4 +167,4 @@ | |||
|     }); | ||||
| </script> | ||||
| </body> | ||||
| </html> | ||||
| </html> | ||||
|  |  | |||
							
								
								
									
										179
									
								
								templates/index.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										179
									
								
								templates/index.html
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,179 @@ | |||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
| <head> | ||||
|     <meta charset="utf-8"> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> | ||||
| 
 | ||||
|     <!-- Bootstrap CSS --> | ||||
|     <link rel="stylesheet" href="css/styles.min.css" | ||||
|           integrity="sha384-z6vVrRFOae08oK23yt6itLI8bfPDebhJw60IbTu43zFoAELolv/CiNUBScry21Fa" crossorigin="anonymous"> | ||||
|     <meta name="theme-color" content="#ffffff"> | ||||
| 
 | ||||
|     <title>{{ .Title }}</title> | ||||
| </head> | ||||
| <body> | ||||
| <div class="container"> | ||||
|     <h1>{{ .Title }}</h1> | ||||
|     <div class="row"> | ||||
|         <div class="col-12"> | ||||
|             <form id="csr-form"> | ||||
|                 <div class="form-group"> | ||||
|                     <label for="nameInput">{{ .NameLabel }}</label> | ||||
|                     <input type="text" class="form-control" id="nameInput" aria-describedby="nameHelp" required | ||||
|                            minlength="3"> | ||||
|                     <small id="nameHelp" class="form-text text-muted">{{ .NameHelpText }}</small> | ||||
|                 </div> | ||||
|                 <div class="form-group"> | ||||
|                     <label for="passwordInput">{{ .PasswordLabel }}</label> | ||||
|                     <input type="password" class="form-control" id="passwordInput" aria-describedby="nameHelp" required | ||||
|                            minlength="8"> | ||||
|                 </div> | ||||
|                 <fieldset class="form-group"> | ||||
|                     <legend>{{ .RSAKeySizeLegend }}</legend> | ||||
|                     <div class="form-check"> | ||||
|                         <input class="form-check-input" type="radio" name="keySize" id="size3072" value="3072" checked> | ||||
|                         <label class="form-check-label" for="size3072">{{ .RSA3072Label }}</label> | ||||
|                     </div> | ||||
|                     <div class="form-check"> | ||||
|                         <input class="form-check-input" type="radio" name="keySize" id="size2048" value="2048"> | ||||
|                         <label class="form-check-label" for="size2048">{{ .RSA2048Label }}</label> | ||||
|                     </div> | ||||
|                     <div class="form-check"> | ||||
|                         <input class="form-check-input" type="radio" name="keySize" id="size4096" value="4096"> | ||||
|                         <label class="form-check-label" for="size4096">{{ .RSA4096Label }}</label> | ||||
|                     </div> | ||||
|                     <small id="keySizeHelp" class="form-text text-muted">{{ .RSAHelpText }}</small> | ||||
|                 </fieldset> | ||||
|                 <button type="submit" id="gen-csr-button" class="btn btn-primary">{{ .CSRButtonLabel }}</button> | ||||
|             </form> | ||||
|         </div> | ||||
|     </div> | ||||
|     <div id="status-block" class="d-none row"> | ||||
|         <div class="col-12"> | ||||
|             <div class="d-flex align-items-center"> | ||||
|                 <strong id="status-text">{{ .StatusLoading }}</strong> | ||||
|                 <div class="spinner-border ml-auto" id="status-spinner" role="status" aria-hidden="true"></div> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
|     <div class="row"> | ||||
|         <div class="col-12"> | ||||
|             <div id="result"> | ||||
|                 <button type="button" disabled id="send-button" class="btn btn-default disabled"> | ||||
|                     {{ .SendCSRButtonLabel }} | ||||
|                 </button> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
|     <pre id="key"></pre> | ||||
|     <pre id="csr"></pre> | ||||
|     <pre id="crt"></pre> | ||||
| </div> | ||||
| <script src="js/jquery.min.js" integrity="sha384-ZvpUoO/+PpLXR1lu4jmpXWu80pZlYUAfxl5NsBMWOEPSjUn/6Z/hRTt8+pR6L4N2" | ||||
|         crossorigin="anonymous"></script> | ||||
| <script src="js/forge.all.min.js" integrity="sha384-VfWVy4csHnuL0Tq/vQkZtIpDf4yhSLNf3aBffGj3wKUmyn1UPNx4v0Pzo9chiHu1" | ||||
|         crossorigin="anonymous"></script> | ||||
| <script src="js/bootstrap.bundle.min.js" | ||||
|         integrity="sha384-ho+j7jyWK8fNQe+A12Hb8AhRq26LrZ/JpcUGGOn+Y7RsweNrtN/tE3MoK7ZeZDyx" | ||||
|         crossorigin="anonymous"></script> | ||||
| <script src="js/i18next.min.js" integrity="sha384-Juj1kpjwKBUTV6Yp9WHG4GdeoMxCmx0zBN9SkwlyrAh5QYWb3l4WrfG7oTv/b00a" | ||||
|         crossorigin="anonymous"></script> | ||||
| <script> | ||||
|     async function postData(url = '', data = {}) { | ||||
|         const response = await fetch(url, { | ||||
|             method: 'POST', | ||||
|             mode: 'cors', | ||||
|             cache: 'no-cache', | ||||
|             credentials: 'same-origin', | ||||
|             headers: { | ||||
|                 'Content-Type': 'application/json', | ||||
|             }, | ||||
|             redirect: "error", | ||||
|             referrerPolicy: "no-referrer", | ||||
|             body: JSON.stringify(data), | ||||
|         }); | ||||
|         return response.json() | ||||
|     } | ||||
| 
 | ||||
|     document.addEventListener("DOMContentLoaded", function () { | ||||
|         i18n.init({fallbackLng: 'en', debug: true}, (err) => { | ||||
|             if (err) return console.log('something went wrong loading', err); | ||||
|         }); | ||||
| 
 | ||||
|         const keyElement = document.getElementById('key'); | ||||
|         document.getElementById('csr-form').onsubmit = function (event) { | ||||
|             const subject = event.target["nameInput"].value; | ||||
|             const password = event.target["passwordInput"].value; | ||||
|             const keySize = parseInt(event.target["keySize"].value); | ||||
|             if (isNaN(keySize)) { | ||||
|                 return false; | ||||
|             } | ||||
|             const spinner = document.getElementById('status-spinner'); | ||||
|             const statusText = document.getElementById('status-text'); | ||||
|             const statusBlock = document.getElementById('status-block'); | ||||
|             statusBlock.classList.remove('d-none'); | ||||
|             spinner.classList.remove('d-none'); | ||||
| 
 | ||||
|             const state = forge.pki.rsa.createKeyPairGenerationState(keySize, 0x10001); | ||||
|             statusText.innerHTML = i18n.t('keygen.started'); | ||||
|             const startDate = new Date(); | ||||
|             const step = function () { | ||||
|                 let duration = (new Date()).getTime() - startDate.getTime(); | ||||
|                 let seconds = Math.floor(duration / 100) / 10; | ||||
|                 if (!forge.pki.rsa.stepKeyPairGenerationState(state, 100)) { | ||||
|                     setTimeout(step, 1); | ||||
|                     statusText.innerHTML = i18n.t('keygen.running', {seconds: seconds}); // `key generation running for ${seconds} seconds`; | ||||
|                 } else { | ||||
|                     statusText.innerHTML = i18n.t('keygen.generated', {seconds: seconds}); // `` | ||||
|                     spinner.classList.add('d-none'); | ||||
|                     const keys = state.keys; | ||||
|                     keyElement.innerHTML = forge.pki.privateKeyToPem(keys.privateKey); | ||||
|                     const csr = forge.pki.createCertificationRequest(); | ||||
| 
 | ||||
|                     csr.publicKey = keys.publicKey; | ||||
|                     csr.setSubject([{ | ||||
|                         name: 'commonName', | ||||
|                         value: subject, | ||||
|                         valueTagClass: forge.asn1.Type.UTF8, | ||||
|                     }]); | ||||
|                     csr.sign(keys.privateKey, forge.md.sha256.create()); | ||||
| 
 | ||||
|                     const verified = csr.verify(); | ||||
|                     if (verified) { | ||||
|                         let csrPem = forge.pki.certificationRequestToPem(csr); | ||||
|                         document.getElementById("csr").innerHTML = csrPem; | ||||
|                         const sendButton = | ||||
|                             document.getElementById("send-button"); | ||||
|                         sendButton.addEventListener("click", function () { | ||||
|                             postData("/sign/", {"csr": csrPem, "commonName": subject}) | ||||
|                                 .then(data => { | ||||
|                                     console.log(data); | ||||
|                                     document.getElementById("crt").innerHTML = data["certificate"]; | ||||
|                                     const certificate = forge.pki.certificateFromPem(data["certificate"]); | ||||
|                                     // browsers have trouble importing anything but 3des encrypted PKCS#12 | ||||
|                                     const p12asn1 = forge.pkcs12.toPkcs12Asn1( | ||||
|                                         keys.privateKey, certificate, password, | ||||
|                                         {algorithm: '3des'} | ||||
|                                     ); | ||||
|                                     const p12Der = forge.asn1.toDer(p12asn1).getBytes(); | ||||
|                                     const p12B64 = forge.util.encode64(p12Der); | ||||
| 
 | ||||
|                                     const a = document.createElement('a'); | ||||
|                                     a.download = 'client_certificate.p12'; | ||||
|                                     a.setAttribute('href', 'data:application/x-pkcs12;base64,' + p12B64); | ||||
|                                     a.appendChild(document.createTextNode("Download")); | ||||
|                                     document.getElementById('result').appendChild(a); | ||||
|                                 }); | ||||
|                         }); | ||||
|                         sendButton.removeAttribute("disabled"); | ||||
|                         sendButton.classList.remove("disabled"); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             setTimeout(step); | ||||
|             return false; | ||||
|         }; | ||||
|     }); | ||||
| </script> | ||||
| </body> | ||||
| </html> | ||||
		Reference in a new issue