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