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, | ||||||
|  |  | ||||||
							
								
								
									
										7718
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										7718
									
								
								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