Skip to content

Instantly share code, notes, and snippets.

@liggitt
Last active April 15, 2016 18:10
Show Gist options
  • Save liggitt/535f6529e7efaeb6faef38434f98c3aa to your computer and use it in GitHub Desktop.
Save liggitt/535f6529e7efaeb6faef38434f98c3aa to your computer and use it in GitHub Desktop.
Test case for client-cert CORS requests
package main
import (
"crypto/tls"
"crypto/x509"
"fmt"
"net"
"net/http"
"os"
"time"
)
// Download and install go from https://golang.org/dl/
// Launch with:
// go run client-cert-test.go
func main() {
go serve("9080")
go serveTLS("9443", true)
go serveTLS("9444", false)
fmt.Println(`Launch http://127.0.0.1:9080/`)
fmt.Println(`ctrl+c to exit`)
select {}
}
// launch a non-tls server on the given port
// mostly for setup instructions, serving the CA to trust, and the launch page for the tests
func serve(port string) {
server := &http.Server{
Addr: "0.0.0.0:" + port,
Handler: http.HandlerFunc(handle(port)),
}
fmt.Println(server.ListenAndServe())
os.Exit(1)
}
// launch a tls server on the given port
// optionally request client certificates during the handshake
func serveTLS(port string, requestClientCert bool) {
servingCert, _ := tls.X509KeyPair(serverCertData, serverKeyData)
server := &http.Server{
Addr: "0.0.0.0:" + port,
Handler: http.HandlerFunc(handle(port)),
TLSConfig: &tls.Config{
NextProtos: []string{"http/1.1"},
Certificates: []tls.Certificate{servingCert},
},
}
// Make sure every request has to do a TLS handshake
// Otherwise, incidental visits to the api host can make the bug intermittently reproduceable
server.SetKeepAlivesEnabled(false)
if requestClientCert {
clientCAPool := x509.NewCertPool()
clientCAPool.AppendCertsFromPEM(serverCAData)
server.TLSConfig.ClientCAs = clientCAPool
server.TLSConfig.ClientAuth = tls.RequestClientCert
}
ln, err := net.Listen("tcp", server.Addr)
if err != nil {
panic(err)
}
l := &debuggingListener{tls.NewListener(ln, server.TLSConfig)}
fmt.Println(server.Serve(l))
os.Exit(1)
}
// Wrap a net.Listener to log when Accept calls are made
type debuggingListener struct {
net.Listener
}
func (d *debuggingListener) Accept() (net.Conn, error) {
conn, err := d.Listener.Accept()
fmt.Printf("Accept connection: %v\n", err)
return conn, err
}
// handle all requests
func handle(port string) func(w http.ResponseWriter, req *http.Request) {
return func(w http.ResponseWriter, req *http.Request) {
fmt.Printf("%s %s\n%#v\n\n", req.Method, req.URL, req.Header)
switch req.URL.Path {
case "/":
// Serve setup HTML
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(setupHTML))
case "/ca.crt":
// Serve CA cert so browsers can accept it
w.Header().Set("Content-Type", "application/x-x509-ca-cert")
w.Write(serverCAData)
case "/test":
// Serve test case HTML
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(fmt.Sprintf(testHTML, port)))
default:
// Serve plaintext test data from all other paths
// Add CORS headers to allow cross-domain requests
origin := req.Header.Get("Origin")
if origin != "" {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, X-Requested-With, If-Modified-Since")
w.Header().Set("Access-Control-Allow-Credentials", "true")
}
if req.Method == "OPTIONS" {
w.WriteHeader(http.StatusNoContent)
} else {
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
w.Write([]byte(fmt.Sprintf(`%d.%d`, time.Now().Second(), time.Now().Nanosecond())))
}
}
}
}
var (
setupHTML = `<!doctype html>
<html lang="en">
<head>
<style>
* { font-family: sans-serif }
table { width: 100% }
iframe { width: 100%; height: 200px; }
</style>
</head>
<body>
<ol>
<li>Alias api.example.com and web.example.com to 127.0.0.1 (using <code>/etc/hosts</code> or <code>%WINDOWS%\system32\drivers\etc\hosts</code>)
<li>Install <a href="/ca.crt" target="_blank">ca.crt</a> to the Trusted Root Certification Authorities certificate store (in a throwaway VM or test machine, not on a real machine!). The frames below should load without security warnings.
<li>Try both cross-origin test cases. The only difference between them is the client cert request.
<li>Try both same-origin test cases. The only difference between them is the client cert request, and both succeed.
</ol>
<p>If the "same-origin with client cert request" test case is loaded before running the "cross-origin with client cert request" test, the cross-origin test will pass, despite each request starting from scratch with a TLS handshake (keep-alive is disabled in the test server)</p>
<table>
<tr>
<th><a href="https://web.example.com:9443/test" target="cross_cert">Cross-origin XHR with client cert request</a>
<th><a href="https://web.example.com:9444/test" target="cross_nocert">Cross-origin XHR without client cert request</a>
<tr>
<td><iframe name="cross_cert"></iframe>
<td><iframe name="cross_nocert"></iframe>
<tr>
<th><a href="https://api.example.com:9443/test" target="same_cert">Same-origin XHR with client cert request</a>
<th><a href="https://api.example.com:9444/test" target="same_nocert">Same-origin XHR without client cert request</a>
<tr>
<td><iframe name="same_cert"></iframe>
<td><iframe name="same_nocert"></iframe>
</table>
</body>
</html>`
testHTML = `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.2/jquery.min.js"></script>
<script>
function request(method, outputTarget) {
var target = $(outputTarget);
target.empty().append("<div>Requesting "+method+"...</div>");
$.ajax({
method: method,
url: "https://api.example.com:%[1]s/api/" + (new Date().getTime()),
headers: {"Authorization":"Bearer 123"}
})
.done(function(){
console.log(arguments);
$("<div></div>").text("Success: " + JSON.stringify(arguments)).appendTo(target);
})
.fail(function(){
console.log(arguments);
$("<div></div>").text("Fail: " + JSON.stringify(arguments)).appendTo(target);
});
}
$(function(){
$("#origin").text(document.location.toString());
})
</script>
</head>
<body>
Serving from <span id="origin"></span><br>
<button onclick="request('GET','#result')">Make GET XHR request to https://api.example.com:%[1]s/</button><br>
<button onclick="request('POST','#result')">Make POST XHR request to https://api.example.com:%[1]s/</button><br>
<div id="result"></div>
</body>
</html>
`
serverCAData = []byte(`
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 1 (0x1)
Signature Algorithm: sha256WithRSAEncryption
Issuer: CN=test-ca
Validity
Not Before: Apr 13 14:45:54 2016 GMT
Not After : Mar 20 14:45:55 2116 GMT
Subject: CN=test-ca
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
RSA Public Key: (2048 bit)
Modulus (2048 bit):
00:cb:52:6a:a0:cf:86:ff:dd:bd:a4:38:32:71:6e:
cc:85:bd:7a:be:03:8d:b1:3b:f8:c0:dd:eb:21:96:
ef:9b:5d:b7:d1:f5:e5:d8:6c:8e:68:f6:80:f6:9b:
19:3c:84:05:a8:3c:ae:da:c8:df:17:23:32:52:bf:
92:13:0c:9e:b2:f3:ce:18:83:fd:2d:6c:ef:b8:35:
32:45:90:15:6c:06:64:9f:81:c4:df:74:cc:96:a4:
75:e4:c4:fa:ad:b4:f3:fd:ba:36:22:01:c9:9e:76:
58:42:a3:80:c0:64:7e:c0:b0:90:84:88:b4:a0:59:
fe:43:ef:91:1e:1a:7a:92:e6:09:cd:dd:4c:8e:d1:
ea:4b:0f:0f:b5:48:b1:77:9f:06:38:29:08:ba:6c:
34:56:58:a7:52:dd:64:3a:28:17:78:72:69:8f:f6:
16:f8:e4:64:03:7f:35:92:7b:4f:23:83:07:48:29:
27:de:f3:ae:a9:84:48:98:35:9b:29:f4:01:bf:37:
12:f5:dc:40:0c:99:5f:81:44:8f:17:94:25:d1:65:
19:35:a8:82:57:2b:2c:4a:ae:b1:3d:47:76:3d:aa:
3e:f0:3b:68:3c:68:82:81:6f:62:68:2f:85:b6:47:
5c:fc:68:d9:be:b5:7c:da:6d:62:36:47:cf:3b:fd:
56:57
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Key Usage: critical
Digital Signature, Key Encipherment, Certificate Sign
X509v3 Basic Constraints: critical
CA:TRUE
Signature Algorithm: sha256WithRSAEncryption
97:d1:6d:3f:3f:6b:a6:cb:40:30:a5:ab:cd:2d:c6:8e:70:07:
6f:f1:69:b4:0c:d6:0c:a8:cc:98:3d:47:d0:9e:f1:6a:7c:c3:
07:4e:5b:c6:47:64:d9:70:5f:6c:6f:a8:c0:0c:ad:14:c8:33:
be:f0:26:67:ac:43:b9:c2:5f:b1:29:ff:c3:69:2d:a1:72:62:
b4:1a:e4:20:0d:0d:88:41:4a:90:2d:f7:5c:ab:ed:23:55:28:
f6:dd:aa:37:a5:ad:15:ab:dc:18:9d:c6:e7:a0:30:48:ed:ee:
91:0f:49:b8:dd:ef:78:29:01:0e:c0:37:06:59:99:7e:39:e8:
9c:80:38:1f:4d:aa:b6:aa:5e:ec:35:ee:01:d9:45:01:5f:69:
da:f5:aa:bc:5d:b6:a0:ce:cb:fc:4e:e4:5f:c6:42:a0:bd:91:
a1:99:b1:95:f5:86:a2:0e:29:98:0b:6e:d3:8b:e4:f6:1c:bc:
24:39:4f:67:12:3c:dc:69:77:38:57:d4:19:da:9e:98:59:c2:
c3:84:39:26:ab:6d:5a:6e:1a:51:27:27:43:35:97:8c:8e:27:
1f:e6:c6:24:ec:ef:4f:b4:63:e0:c5:f2:cf:43:0a:b6:7a:df:
1c:90:87:15:6d:a2:7e:02:d4:9d:90:e7:ba:6e:6f:d3:11:89:
24:92:31:b5
-----BEGIN CERTIFICATE-----
MIICxDCCAaygAwIBAgIBATANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDEwd0ZXN0
LWNhMCAXDTE2MDQxMzE0NDU1NFoYDzIxMTYwMzIwMTQ0NTU1WjASMRAwDgYDVQQD
Ewd0ZXN0LWNhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAy1JqoM+G
/929pDgycW7Mhb16vgONsTv4wN3rIZbvm1230fXl2GyOaPaA9psZPIQFqDyu2sjf
FyMyUr+SEwyesvPOGIP9LWzvuDUyRZAVbAZkn4HE33TMlqR15MT6rbTz/bo2IgHJ
nnZYQqOAwGR+wLCQhIi0oFn+Q++RHhp6kuYJzd1MjtHqSw8PtUixd58GOCkIumw0
VlinUt1kOigXeHJpj/YW+ORkA381kntPI4MHSCkn3vOuqYRImDWbKfQBvzcS9dxA
DJlfgUSPF5Ql0WUZNaiCVyssSq6xPUd2Pao+8DtoPGiCgW9iaC+Ftkdc/GjZvrV8
2m1iNkfPO/1WVwIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAqQwDwYDVR0TAQH/BAUw
AwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAl9FtPz9rpstAMKWrzS3GjnAHb/FptAzW
DKjMmD1H0J7xanzDB05bxkdk2XBfbG+owAytFMgzvvAmZ6xDucJfsSn/w2ktoXJi
tBrkIA0NiEFKkC33XKvtI1Uo9t2qN6WtFavcGJ3G56AwSO3ukQ9JuN3veCkBDsA3
BlmZfjnonIA4H02qtqpe7DXuAdlFAV9p2vWqvF22oM7L/E7kX8ZCoL2RoZmxlfWG
og4pmAtu04vk9hy8JDlPZxI83Gl3OFfUGdqemFnCw4Q5JqttWm4aUScnQzWXjI4n
H+bGJOzvT7Rj4MXyz0MKtnrfHJCHFW2ifgLUnZDnum5v0xGJJJIxtQ==
-----END CERTIFICATE-----`)
serverCertData = []byte(`
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 2 (0x2)
Signature Algorithm: sha256WithRSAEncryption
Issuer: CN=test-ca
Validity
Not Before: Apr 13 14:46:21 2016 GMT
Not After : Mar 20 14:46:22 2116 GMT
Subject: CN=api.example.com
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
RSA Public Key: (2048 bit)
Modulus (2048 bit):
00:bf:4c:26:e6:f5:7c:a8:55:56:6d:b6:b4:02:88:
4e:69:41:e9:f6:6e:1d:02:f1:25:a4:46:85:49:90:
10:bc:a7:d9:ec:98:b9:7a:c5:b9:9c:97:61:00:c1:
7e:83:cd:3f:a5:eb:55:ba:20:86:c4:08:aa:34:06:
8e:1b:2f:13:e8:f3:13:35:d4:a2:1e:76:7b:cb:3b:
17:06:b6:64:bc:75:d7:f5:9b:da:2d:b5:c9:7a:a4:
51:e1:46:5b:dc:2c:e6:32:c8:04:94:52:f6:71:c0:
ae:04:d9:8e:bc:95:54:5b:c6:d9:d9:f9:19:b1:08:
30:03:54:20:34:9d:c5:e8:ca:44:8d:f8:c7:a1:f6:
41:35:a1:8d:85:b0:06:1c:46:53:ce:a3:93:47:e6:
0f:ec:bd:f5:bd:d8:ec:dc:67:a8:a0:37:f9:44:90:
26:11:75:c2:18:8a:4c:64:13:f0:2e:24:e0:8a:87:
be:fd:f3:17:32:56:73:37:71:54:c9:75:0c:7d:8a:
15:fb:74:bb:04:c0:b8:14:06:18:6f:bb:a4:c9:6c:
a6:5d:37:5b:91:f2:3e:6d:26:cd:33:fa:72:19:89:
f1:b3:86:b8:47:79:ae:08:ec:89:57:5c:d5:e4:bd:
2a:bb:73:a3:08:a6:94:c4:98:3e:6e:8f:a5:63:9f:
c1:57
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Key Usage: critical
Digital Signature, Key Encipherment
X509v3 Extended Key Usage:
TLS Web Server Authentication
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Subject Alternative Name:
DNS:api.example.com, DNS:web.example.com
Signature Algorithm: sha256WithRSAEncryption
05:0c:d2:60:5c:23:fd:b2:af:c6:bd:2a:bb:f9:a3:9c:77:eb:
dc:39:d5:47:6a:46:40:cb:fc:c9:aa:b4:01:0f:3a:89:44:4f:
24:1e:a8:0f:29:36:3b:0b:16:ae:b9:a6:92:ef:78:18:b8:5b:
cc:ae:75:f0:d1:44:5d:df:6f:71:c4:c1:50:ff:c7:8f:65:5c:
da:2b:71:2e:ad:79:81:86:40:6d:32:d6:c0:80:9a:55:cd:0d:
d4:0e:09:80:42:11:38:06:37:f2:a4:f4:19:a9:7f:07:1a:fa:
ee:66:8a:3a:61:08:20:6f:41:87:d4:78:1c:3e:50:47:15:74:
78:6a:88:35:f6:97:d2:09:1e:c4:80:6a:8c:97:b2:a7:5b:16:
9f:ca:8d:82:4d:b3:60:e4:90:28:55:fa:f1:47:1e:8c:e2:03:
44:7f:2f:f4:55:80:b7:b2:22:4f:df:43:f1:de:8f:16:ba:af:
6a:7c:08:21:2e:47:39:82:01:a5:05:21:62:f9:0c:8c:2a:27:
fe:72:f7:32:6d:c9:44:73:4c:b2:e5:aa:72:82:26:9d:fe:61:
c5:40:09:c0:4b:04:3f:7e:71:d0:0c:8e:7b:b4:db:c8:59:85:
8a:d1:1c:92:cb:b4:f1:f3:6f:fe:e8:ce:d8:d7:79:a1:1e:98:
d6:89:be:af
-----BEGIN CERTIFICATE-----
MIIDCzCCAfOgAwIBAgIBAjANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDEwd0ZXN0
LWNhMCAXDTE2MDQxMzE0NDYyMVoYDzIxMTYwMzIwMTQ0NjIyWjAaMRgwFgYDVQQD
Ew9hcGkuZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
AQC/TCbm9XyoVVZttrQCiE5pQen2bh0C8SWkRoVJkBC8p9nsmLl6xbmcl2EAwX6D
zT+l61W6IIbECKo0Bo4bLxPo8xM11KIednvLOxcGtmS8ddf1m9ottcl6pFHhRlvc
LOYyyASUUvZxwK4E2Y68lVRbxtnZ+RmxCDADVCA0ncXoykSN+Meh9kE1oY2FsAYc
RlPOo5NH5g/svfW92OzcZ6igN/lEkCYRdcIYikxkE/AuJOCKh7798xcyVnM3cVTJ
dQx9ihX7dLsEwLgUBhhvu6TJbKZdN1uR8j5tJs0z+nIZifGzhrhHea4I7IlXXNXk
vSq7c6MIppTEmD5uj6Vjn8FXAgMBAAGjYjBgMA4GA1UdDwEB/wQEAwIFoDATBgNV
HSUEDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMCsGA1UdEQQkMCKCD2FwaS5l
eGFtcGxlLmNvbYIPd2ViLmV4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQAF
DNJgXCP9sq/GvSq7+aOcd+vcOdVHakZAy/zJqrQBDzqJRE8kHqgPKTY7CxauuaaS
73gYuFvMrnXw0URd329xxMFQ/8ePZVzaK3EurXmBhkBtMtbAgJpVzQ3UDgmAQhE4
BjfypPQZqX8HGvruZoo6YQggb0GH1HgcPlBHFXR4aog19pfSCR7EgGqMl7KnWxaf
yo2CTbNg5JAoVfrxRx6M4gNEfy/0VYC3siJP30Px3o8Wuq9qfAghLkc5ggGlBSFi
+QyMKif+cvcybclEc0yy5apygiad/mHFQAnASwQ/fnHQDI57tNvIWYWK0RySy7Tx
82/+6M7Y13mhHpjWib6v
-----END CERTIFICATE-----`)
serverKeyData = []byte(`
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAv0wm5vV8qFVWbba0AohOaUHp9m4dAvElpEaFSZAQvKfZ7Ji5
esW5nJdhAMF+g80/petVuiCGxAiqNAaOGy8T6PMTNdSiHnZ7yzsXBrZkvHXX9Zva
LbXJeqRR4UZb3CzmMsgElFL2ccCuBNmOvJVUW8bZ2fkZsQgwA1QgNJ3F6MpEjfjH
ofZBNaGNhbAGHEZTzqOTR+YP7L31vdjs3GeooDf5RJAmEXXCGIpMZBPwLiTgioe+
/fMXMlZzN3FUyXUMfYoV+3S7BMC4FAYYb7ukyWymXTdbkfI+bSbNM/pyGYnxs4a4
R3muCOyJV1zV5L0qu3OjCKaUxJg+bo+lY5/BVwIDAQABAoIBADUZ3nKeEkxn4+Xw
oWdSjvGI6nkNd+ApMFm5eaZB52N29HdIrbP2zt845iRfkc7kWpakDNftz3r7LMPk
Te4d01kGoH6A17+9BAAWFv42AyCNVbVH3fhyTctNca0m6rjjfcL64sqJfP92jNer
zINssE4JlM985jTOIQXLhGUWpqlHgX+EtLTRhQ+3NsCWjpKlfMVTm6AoISI8rjaZ
JjyZwgVx5zjGnfZZIVlz5Ej6tGBq2S/VSZihV/RdrCQXhPX1cODewN0S3CQNzuK0
Ht8m6+wGb+uk+hxHuxi7hySaXHYWo4pjyydCElnGeb7iXlJ/7/eA0qmC5533JSff
jOrnFQECgYEA4TlVfTKC7+hDwvK04kbyJTrjEhRH4LN387yNA3iHhU60vlwif4/7
jaryE/iU8AMN6dlsoUJB+pVH4fThJT6vjM/QEdUNdANJMJvtk0KjIjfJnUGVJ74B
DLXlM//rS1JIM+qrjCjb4aN8woQNsTT8NaZiYgHCLmg/0wdJqmzdLu8CgYEA2XAE
8rdpfzdajFJmUzYWf3r6zmBe5ecfgAuMzPCYyJe0lfHqSnXlC2nrG96QX+1ojzIj
/Vceh0GQswIeIWSS99JukLRsU4Rd0fzk8BQFqprVwJQ/R2YKiGTBtni1wBoR34sP
jiGVG3vT66zcze8L5h4P/DWlV0Ne57PCbEtclBkCgYEApMR2B16RrgNkt1UqAbRX
Z+c5wbs2jmudYKHbI+PkeSEIV4896cajCJQ7/2JHS4NghWj78MlxTWoyqVql78J5
WXGazcDo06unurkISEhi4iCgDbyx6t41FGBp6u3Z7EOo8NpIYARwQBWDqyZCgha6
QGGV7g9NSPgZYUAeo2B7O98CgYASGTfgOBII68OWsHkh7fubatIbgXwEqOM/VjbH
DDO7Zp06aeN1hTCmbY+LR1A/G9S7LpI+URUSbwurSr7VSrjM9fAMDWiC3x6sDt/D
d/csxyyJlg1aVQ0FY1WYaZ2/OqxILhwCWZs+qWTvVfkfDwmvgssT1CdKByqMILNL
Lk6raQKBgHjdXmKCA4Eu7dyCbb6BN8IQHckFuqdoDSjMeGQq/3o2G6onY+UwgQ/H
95O18lIVcQE7Ng1gNYL17m55h9JKyR1dkYodKtB1CQLzYb4lrByPPJFuLVpu1xZP
Y3AmKViLO1tZVdR4455in9Grc1qEmZPocAP3uwr4ZzIaq4jE2Zml
-----END RSA PRIVATE KEY-----`)
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment