Implement CSRF protection
This commit adds CSRF protection based on the gorilla/csrf package. Node dependencies have been updated. Logging uses sirupsen/logrus for log level support now.
This commit is contained in:
parent
e13c9d174b
commit
1f8c44689e
6 changed files with 7088 additions and 710 deletions
2
go.mod
2
go.mod
|
@ -4,7 +4,9 @@ go 1.13
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/BurntSushi/toml v0.3.1
|
github.com/BurntSushi/toml v0.3.1
|
||||||
|
github.com/gorilla/csrf v1.7.0
|
||||||
github.com/nicksnyder/go-i18n/v2 v2.1.1
|
github.com/nicksnyder/go-i18n/v2 v2.1.1
|
||||||
|
github.com/sirupsen/logrus v1.7.0
|
||||||
golang.org/x/text v0.3.4
|
golang.org/x/text v0.3.4
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
16
go.sum
16
go.sum
|
@ -1,7 +1,23 @@
|
||||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/gorilla/csrf v1.7.0 h1:mMPjV5/3Zd460xCavIkppUdvnl5fPXMpv2uz2Zyg7/Y=
|
||||||
|
github.com/gorilla/csrf v1.7.0/go.mod h1:+a/4tCmqhG6/w4oafeAZ9pEa3/NZOWYVbD9fV0FwIQA=
|
||||||
|
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||||
|
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||||
github.com/nicksnyder/go-i18n/v2 v2.1.1 h1:ATCOanRDlrfKVB4WHAdJnLEqZtDmKYsweqsOUYflnBU=
|
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=
|
github.com/nicksnyder/go-i18n/v2 v2.1.1/go.mod h1:d++QJC9ZVf7pa48qrsRWhMJ5pSHIPmS3OLqK1niyLxs=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
|
||||||
|
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||||
|
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
|
||||||
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
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 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=
|
||||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
|
44
main.go
44
main.go
|
@ -2,19 +2,22 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/rand"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/BurntSushi/toml"
|
"github.com/BurntSushi/toml"
|
||||||
|
"github.com/gorilla/csrf"
|
||||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -32,13 +35,28 @@ type responseData struct {
|
||||||
func (h *signCertificate) sign(csrPem string, commonName string) (certPem string, err error) {
|
func (h *signCertificate) sign(csrPem string, commonName string) (certPem string, err error) {
|
||||||
log.Printf("received CSR for %s:\n\n%s", commonName, csrPem)
|
log.Printf("received CSR for %s:\n\n%s", commonName, csrPem)
|
||||||
subjectDN := fmt.Sprintf("/CN=%s", commonName)
|
subjectDN := fmt.Sprintf("/CN=%s", commonName)
|
||||||
err = ioutil.WriteFile("in.pem", []byte(csrPem), 0644)
|
var csrFile *os.File
|
||||||
if err != nil {
|
if csrFile, err = ioutil.TempFile("", "*.csr.pem"); err != nil {
|
||||||
log.Print(err)
|
log.Errorf("could not open temporary file: %s", err)
|
||||||
return
|
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(
|
opensslCommand := exec.Command(
|
||||||
"openssl", "ca", "-config", "ca.cnf", "-days", "365",
|
"openssl", "ca", "-config", "ca.cnf",
|
||||||
"-policy", "policy_match", "-extensions", "client_ext",
|
"-policy", "policy_match", "-extensions", "client_ext",
|
||||||
"-batch", "-subj", subjectDN, "-utf8", "-rand_serial", "-in", "in.pem")
|
"-batch", "-subj", subjectDN, "-utf8", "-rand_serial", "-in", "in.pem")
|
||||||
var out, cmdErr bytes.Buffer
|
var out, cmdErr bytes.Buffer
|
||||||
|
@ -159,6 +177,7 @@ func (i *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
"CSRButtonLabel": csrButtonLabel,
|
"CSRButtonLabel": csrButtonLabel,
|
||||||
"StatusLoading": statusLoading,
|
"StatusLoading": statusLoading,
|
||||||
"SendCSRButtonLabel": sendCSRButtonLabel,
|
"SendCSRButtonLabel": sendCSRButtonLabel,
|
||||||
|
csrf.TemplateTag: csrf.TemplateField(r),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Panic(err)
|
log.Panic(err)
|
||||||
|
@ -207,6 +226,18 @@ func (j *jsLocalesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 main() {
|
func main() {
|
||||||
tlsConfig := &tls.Config{
|
tlsConfig := &tls.Config{
|
||||||
CipherSuites: []uint16{
|
CipherSuites: []uint16{
|
||||||
|
@ -227,6 +258,7 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
csrfKey := generateRandomBytes(32)
|
||||||
mux.Handle("/sign/", &signCertificate{})
|
mux.Handle("/sign/", &signCertificate{})
|
||||||
mux.Handle("/", &indexHandler{Bundle: bundle})
|
mux.Handle("/", &indexHandler{Bundle: bundle})
|
||||||
fileServer := http.FileServer(http.Dir("./public"))
|
fileServer := http.FileServer(http.Dir("./public"))
|
||||||
|
@ -235,7 +267,7 @@ func main() {
|
||||||
mux.Handle("/locales/", &jsLocalesHandler{Bundle: bundle})
|
mux.Handle("/locales/", &jsLocalesHandler{Bundle: bundle})
|
||||||
server := http.Server{
|
server := http.Server{
|
||||||
Addr: ":8000",
|
Addr: ":8000",
|
||||||
Handler: mux,
|
Handler: csrf.Protect(csrfKey, csrf.FieldName("csrfToken"), csrf.RequestHeader("X-CSRF-Token"))(mux),
|
||||||
TLSConfig: tlsConfig,
|
TLSConfig: tlsConfig,
|
||||||
ReadTimeout: 20 * time.Second,
|
ReadTimeout: 20 * time.Second,
|
||||||
ReadHeaderTimeout: 5 * time.Second,
|
ReadHeaderTimeout: 5 * time.Second,
|
||||||
|
|
7654
package-lock.json
generated
7654
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -61,7 +61,8 @@
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div id="result">
|
<div id="result">
|
||||||
<button type="button" disabled id="send-button" class="btn btn-default disabled">Send signing request</button>
|
<button type="button" disabled id="send-button" class="btn btn-default disabled">Send signing request
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -91,6 +92,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
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');
|
const keyElement = document.getElementById('key');
|
||||||
document.getElementById('csr-form').onsubmit = function (event) {
|
document.getElementById('csr-form').onsubmit = function (event) {
|
||||||
const subject = event.target["nameInput"].value;
|
const subject = event.target["nameInput"].value;
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
<!-- Bootstrap CSS -->
|
<!-- Bootstrap CSS -->
|
||||||
<link rel="stylesheet" href="css/styles.min.css"
|
<link rel="stylesheet" href="css/styles.min.css"
|
||||||
integrity="sha384-z6vVrRFOae08oK23yt6itLI8bfPDebhJw60IbTu43zFoAELolv/CiNUBScry21Fa" crossorigin="anonymous">
|
integrity="sha384-vKuz4xd0kXa+x9wRdibDAVE8gXC/1up2T9QVSas8Rk07AZhzOzbwFdj00XUjOO4i" crossorigin="anonymous">
|
||||||
<meta name="theme-color" content="#ffffff">
|
<meta name="theme-color" content="#ffffff">
|
||||||
|
|
||||||
<title>{{ .Title }}</title>
|
<title>{{ .Title }}</title>
|
||||||
|
@ -17,6 +17,7 @@
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<form id="csr-form">
|
<form id="csr-form">
|
||||||
|
{{ .csrfField }}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="nameInput">{{ .NameLabel }}</label>
|
<label for="nameInput">{{ .NameLabel }}</label>
|
||||||
<input type="text" class="form-control" id="nameInput" aria-describedby="nameHelp" required
|
<input type="text" class="form-control" id="nameInput" aria-describedby="nameHelp" required
|
||||||
|
@ -79,7 +80,7 @@
|
||||||
<script src="js/i18next.min.js" integrity="sha384-Juj1kpjwKBUTV6Yp9WHG4GdeoMxCmx0zBN9SkwlyrAh5QYWb3l4WrfG7oTv/b00a"
|
<script src="js/i18next.min.js" integrity="sha384-Juj1kpjwKBUTV6Yp9WHG4GdeoMxCmx0zBN9SkwlyrAh5QYWb3l4WrfG7oTv/b00a"
|
||||||
crossorigin="anonymous"></script>
|
crossorigin="anonymous"></script>
|
||||||
<script>
|
<script>
|
||||||
async function postData(url = '', data = {}) {
|
async function postData(url = '', data = {}, csrfToken) {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
mode: 'cors',
|
mode: 'cors',
|
||||||
|
@ -87,6 +88,7 @@
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-Token': csrfToken,
|
||||||
},
|
},
|
||||||
redirect: "error",
|
redirect: "error",
|
||||||
referrerPolicy: "no-referrer",
|
referrerPolicy: "no-referrer",
|
||||||
|
@ -104,6 +106,7 @@
|
||||||
document.getElementById('csr-form').onsubmit = function (event) {
|
document.getElementById('csr-form').onsubmit = function (event) {
|
||||||
const subject = event.target["nameInput"].value;
|
const subject = event.target["nameInput"].value;
|
||||||
const password = event.target["passwordInput"].value;
|
const password = event.target["passwordInput"].value;
|
||||||
|
const csrfToken = event.target["csrfToken"].value;
|
||||||
const keySize = parseInt(event.target["keySize"].value);
|
const keySize = parseInt(event.target["keySize"].value);
|
||||||
if (isNaN(keySize)) {
|
if (isNaN(keySize)) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -145,7 +148,7 @@
|
||||||
const sendButton =
|
const sendButton =
|
||||||
document.getElementById("send-button");
|
document.getElementById("send-button");
|
||||||
sendButton.addEventListener("click", function () {
|
sendButton.addEventListener("click", function () {
|
||||||
postData("/sign/", {"csr": csrPem, "commonName": subject})
|
postData("/sign/", {"csr": csrPem, "commonName": subject}, csrfToken)
|
||||||
.then(data => {
|
.then(data => {
|
||||||
console.log(data);
|
console.log(data);
|
||||||
document.getElementById("crt").innerHTML = data["certificate"];
|
document.getElementById("crt").innerHTML = data["certificate"];
|
||||||
|
|
Reference in a new issue