Go proper TLS
Yksinekrtainen pannu
Ominaisuudet:
- HTTP Strict Transport Security on lisätty
- Vain turvallisimmat salausalgoritmit (standardoidut, mukana Go:n TLS-toteutuksessa) ovat sallittuja
- Vain TLS 1.2+ on sallittu
package main
import (
"crypto/tls"
"log"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
w.Header().Add("Strict-Transport-Security", "max-age=15768000; includeSubDomains")
w.Write([]byte("Hello world!\n"))
})
tls_cfg := &tls.Config{
CipherSuites: []uint16{
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
},
CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256},
MinVersion: tls.VersionTLS12,
PreferServerCipherSuites: true,
}
srv := &http.Server{
Addr: ":443",
TLSConfig: tls_cfg,
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler), 0),
}
log.Fatal(srv.ListenAndServeTLS("markonnakkijadata.crt", "markonnakkijadata.key"))
}
SSL Server Test antaa palvelimen TLS-toteutukselle arvosanaksi A+, kun käytössä on tarpeeksi pitkät salausavaimet (RSA >= 4096 tai vastaava).
Hmm. Mitähän sitten..
Varmennepohjainen tunnistautuminen
hhmm
package main
import (
"crypto/tls"
"crypto/x509"
"io/ioutil"
"log"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
w.Header().Add("Strict-Transport-Security", "max-age=15768000; includeSubDomains")
w.Write([]byte("Hello world!\n"))
// Tulostetaan asiakkaan varmenteiden CommonName-attribuutit
// Huom! Asiakas saattaa lähettää myös varmentajien varmenteet
for _, x := range req.TLS.PeerCertificates {
log.Println(x.Subject.CommonName)
}
})
// Ladataan varmentajien varmenteet
capool := x509.NewCertPool()
cacert, err := ioutil.ReadFile("markonnakkijaCA.crt")
if err != nil {
panic(err)
}
capool.AppendCertsFromPEM(cacert)
tls_cfg := &tls.Config{
CipherSuites: []uint16{
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
},
ClientCAs: capool, // Asiakkaiden varmenteiden hyväksytyt varmentajat
ClientAuth: tls.RequireAndVerifyClientCert, // Asiakkaiden täytyy tunnistautua varmenteilla
CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256},
MinVersion: tls.VersionTLS12,
PreferServerCipherSuites: true,
}
srv := &http.Server{
Addr: ":443",
TLSConfig: tls_cfg,
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler), 0),
}
log.Fatal(srv.ListenAndServeTLS("markonnakkijadata.crt", "markonnakkijadata.key"))
}
Sisääntulevan varmenteen parsiminen
hghg
package main
import (
"context"
"crypto/x509"
"encoding/pem"
"fmt"
"net"
"net/http"
"regexp"
"strings"
)
// Avain contextia varten
type key string
const certKey key = "cert"
func HelloCertificate(w http.ResponseWriter, r *http.Request) {
// Varmenne on ladattavissa pyyntöjen kontekstista
varmenne := r.Context().Value(certKey).(x509.Certificate)
fmt.Fprintf(w, "Varmenteen CN: %s", varmenne.Subject.CommonName)
}
func newContextWithCertificate(ctx context.Context, req *http.Request) context.Context {
// Luetaan edusta-webbipalvelimen asettama otsaketieto, jossa varmenteen pitäisi tulla
header := req.Header.Get("TLS_CLIENT_CERT")
// Luetaan pyynnön tehnyt osoite
ip, _, err := net.SplitHostPort(req.RemoteAddr)
if err != nil {
panic("failed to determine client IP: " + err.Error())
}
// Jos otsaketieto puuttuu, keskeytetään käsittely
if header == "" {
panic("Asiakkaalta ei tullut otsaketta TLS_CLIENT_CERT")
}
// Jos pyyntö ei tullut edusta-webbipalvelimelta, keskeytetään käsittely tietoturvasyistä
if ip != "192.168.122.222" {
panic("Pyyntö ei tullut edusta-webbipalvelimelta")
}
// Normalisoidaan otsaketieto
varmenne := normalizePem(req.Header.Get("TLS_CLIENT_CERT"))
// Muunnetaan varmenne ASN.1 DER-muotoon
block, _ := pem.Decode([]byte(varmenne))
if block == nil {
panic("failed to parse certificate PEM")
}
// Parsitaan varmenne Go:n x509-objektiksi
x509cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
panic("failed to parse certificate: " + err.Error())
}
// Tässä vaiheessa voidaan esim. suorittaa lisää varmenteen tarkistuksia, tai
// tehdä kyselyitä käyttäjähakemistoon...
// Palautetaan uusi konteksti, johon on tallennettu asiakkaan varmenne
return context.WithValue(ctx, certKey, *x509cert)
}
// httpd muuttaa varmenteen rivinvaihdot välilyönneiksi
// pem.Decode vaatii, että rivinvaihdot ovat entisellään, joten nopea viritys
func normalizePem(in string) string {
r, _ := regexp.Compile("^---[-]+BEGIN[^-]+---[-]+([^-]*)--[-]+END [^-]+---[-]+$")
if r.MatchString(in) {
rs := r.FindAllStringSubmatch(in, -1)[0]
return "-----BEGIN CERTIFICATE-----\n" +
strings.Replace(strings.TrimSpace(rs[1]), " ", "\n", -1) +
"\n-----END CERTIFICATE-----\n"
}
return in
}
func middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
// Muodostetaan uusi konteksti, jossa on asiakkaan varmenne
ctx := newContextWithCertificate(req.Context(), req)
next.ServeHTTP(rw, req.WithContext(ctx))
})
}
func main() {
HelloCertificateHandler := http.HandlerFunc(HelloCertificate)
// Kääritään varsinainen palvelu middlewaren sisään
http.Handle("/", middleware(HelloCertificateHandler))
http.ListenAndServe(":8080", nil)
}