Key pinning is a technique that can protect clients from rogue or compromised certificate authorities [1, 2, 3]. If you have control over the client and the server, you can bake the server’s public key into the client and bypass (or supplement) trust in certificate authorities.
Many mobile applications on iOS and Android do this using these libraries:
The Chrome and Firefox web browsers also allow pinning with pre-loaded pins and support of the HTTP Public Key Pinning (HPKP) protocol.
There are a number of CLI utilities I use that interact with HTTP APIs, and thought it would be nice if they could support pins. I started looking into how one might do certificate pinning in Golang, and the first option was to add the exact certificate to the tls.Config
trust store:
pemData := []byte(`-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----`)
certs := x509.NewCertPool()
certs.AppendCertsFromPEM(pemData)
client = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: certs,
},
},
}
resp, err := client.Get("https://example.com")
Or you could inspect the certificate chain following the TLS handshake:
client := &http.Client{}
tlsConfig := &tls.Config{InsecureSkipVerify: true}
client.Transport = &http.Transport{
DialTLS: func(network, addr string) (net.Conn, error) {
conn, err := tls.Dial(network, addr, tlsConfig)
if err != nil {
return conn, err
}
// inspect conn.ConnectionState() here
return conn, nil
},
}
resp, err := client.Get("https://google.com")
But knowing what to inspect can be tricky. This gist and xmpp-client provide some examples of what to pin, but it is cumbersome to do so on your own.
I couldn’t find any Golang libraries that make key pinning any easier, so I decided to start my own library for writing HPKP aware clients. This library is aimed at providing:
- HPKP related tools (generate pins, inspect servers)
- A convenience functions for writing clients that support pin verification
To inspect the HPKP headers from the server:
$ hpkp-headers https://github.com
{"Created":1465765483,"MaxAge":5184000,"IncludeSubDomains":true,"Permanent":false,"Sha256Pins":["WoiWRyIOVNa9ihaBciRSC7XHjliYS9VwUGOIud4PB18=","RRM1dGqnDFsCJXBTHky16vi1obOlCgFFn/yOhI/y+ho=","k2v657xBsOVe1PQRwOsHsw3bsGT2VzIqz5K+59sNQws=","K87oWBWM9UZfyddvDfoxL+8lpNyoUB2ptGtn0fv6G2Q=","IQBnNBEiFuhj+8x6X8XLgh01V9Ic5/V3IRQLNFFc7v4=","iie1VXtL7HzAMF+/PVPR9xzT80kQxdZeJ+zduCB3uj0=","LvRiGEjRqfzurezaWuj8Wie2gyHMrW5Q06LspMnox7A="]}
And generate pins from the certs a server presents:
$ hpkp-pins -server=github.com:443
pL1+qb9HTMRZJmuC/bB/ZI9d302BYrrqiVuRyW+DGrU=
RRM1dGqnDFsCJXBTHky16vi1obOlCgFFn/yOhI/y+ho=
Or generate a pin from a PEM-encoded certificate file:
$ hpkp-pins -file=cert.pem
AD4C8VGyUrvmReK+D/PYtH52cYJrG9o7VR+uOZIh1Q0=
pL1+qb9HTMRZJmuC/bB/ZI9d302BYrrqiVuRyW+DGrU=
And finally, how to use the hpkp
package to verify pins as part of your application:
s := hpkp.NewMemStorage()
s.Add("github.com", &hpkp.Header{
Permanent: true,
Sha256Pins: []string{
"WoiWRyIOVNa9ihaBciRSC7XHjliYS9VwUGOIud4PB18=",
"RRM1dGqnDFsCJXBTHky16vi1obOlCgFFn/yOhI/y+ho=",
"k2v657xBsOVe1PQRwOsHsw3bsGT2VzIqz5K+59sNQws=",
"K87oWBWM9UZfyddvDfoxL+8lpNyoUB2ptGtn0fv6G2Q=",
"IQBnNBEiFuhj+8x6X8XLgh01V9Ic5/V3IRQLNFFc7v4=",
"iie1VXtL7HzAMF+/PVPR9xzT80kQxdZeJ+zduCB3uj0=",
"LvRiGEjRqfzurezaWuj8Wie2gyHMrW5Q06LspMnox7A=",
},
})
client := &http.Client{}
client.Transport = &http.Transport{
DialTLS: hpkp.PinOnlyDialer(s),
}
resp, err := client.Get("https://github.com")
If you’re interested in using or improving this package please check it out on Github.