From a17cc4838db684cb9f054f1f7d2309666b04c01d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Hurtado?= Date: Thu, 10 Sep 2020 16:35:41 +0200 Subject: [PATCH] Add ssl/client/i/dn resource. Closes #113 --- docs/source/concepts/resource_tree.rst | 26 +++++ internal/server/data/resource.go | 11 ++ internal/server/data/resource_test.go | 105 +++++++++++++++++- internal/server/data/server.go | 5 +- .../server/data/testdata/client_chain.crt | 46 ++++++++ 5 files changed, 191 insertions(+), 2 deletions(-) create mode 100644 internal/server/data/testdata/client_chain.crt diff --git a/docs/source/concepts/resource_tree.rst b/docs/source/concepts/resource_tree.rst index 62c50ac..1d3409b 100644 --- a/docs/source/concepts/resource_tree.rst +++ b/docs/source/concepts/resource_tree.rst @@ -43,6 +43,10 @@ Overview │ │ └──── content The contents of the file uploaded in the form field │ └──── body HTTP request body │ + |─ ssl + │ └──── client + │ └──── i + │ └──── dn Subject's DN common name coming ina request through SSL with mTLS │─ route │ └──── id Id of the route that matched this request. │ @@ -383,6 +387,28 @@ then, when handling the request: foobar +``/ssl/client/i/dn`` Resource +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The IP address of the host making the incoming request. + +Sample Usage +^^^^^^^^^^^^ + +If the user runs: + +.. code-block:: console + + $ curl --cacert path/to/CAfile --cert path/to/clientcredentials http://kapow.example:8080 + +using a client certificate with DN=subject@example.net then, when handling the request: + +.. code-block:: console + + $ kapow get /ssl/client/i/dn + subject@example.net + + ``/route/id`` Resource ~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/internal/server/data/resource.go b/internal/server/data/resource.go index 1130c2b..af14b49 100644 --- a/internal/server/data/resource.go +++ b/internal/server/data/resource.go @@ -161,6 +161,17 @@ func getRequestFileContent(w http.ResponseWriter, r *http.Request, h *model.Hand } } +func getSSLClietnDN(w http.ResponseWriter, r *http.Request, h *model.Handler) { + if h.Request.TLS == nil { + httperror.ErrorJSON(w, ResourceItemNotFound, http.StatusNotFound) + } else if h.Request.TLS.VerifiedChains == nil { + httperror.ErrorJSON(w, ResourceItemNotFound, http.StatusNotFound) + } else { + w.Header().Add("Content-Type", "application/octet-stream") + _, _ = w.Write([]byte(h.Request.TLS.VerifiedChains[0][0].Subject.CommonName)) + } +} + func getRouteId(w http.ResponseWriter, r *http.Request, h *model.Handler) { w.Header().Add("Content-Type", "application/octet-stream") _, _ = w.Write([]byte(h.Route.ID)) diff --git a/internal/server/data/resource_test.go b/internal/server/data/resource_test.go index 9307124..e19e324 100644 --- a/internal/server/data/resource_test.go +++ b/internal/server/data/resource_test.go @@ -18,7 +18,11 @@ package data import ( "bytes" + "crypto/tls" + "crypto/x509" + "encoding/pem" "errors" + "fmt" "io" "io/ioutil" "mime/multipart" @@ -1137,7 +1141,106 @@ func TestGetRequestFileContent500sWhenHandlerRequestErrors(t *testing.T) { } } -// DOING #113: /request/ssl/client/i/dn +func TestGetSSLClientDNReturns404IfNotHTTPS(t *testing.T) { + h := model.Handler{ + Request: httptest.NewRequest("POST", "/", nil), + Writer: httptest.NewRecorder(), + } + r := httptest.NewRequest("GET", "/not-important-here", nil) + w := httptest.NewRecorder() + + getSSLClietnDN(w, r, &h) + + if res := w.Result(); res.StatusCode != http.StatusNotFound { + t.Errorf("Status code mismatch. Expected: %d, got: %d", http.StatusNotFound, res.StatusCode) + } +} + +func TestGetSSLClientDNReturns404IfHTTPSButNotmTLS(t *testing.T) { + h := model.Handler{ + Request: httptest.NewRequest("POST", "https://www.foo.bar:8080/", nil), + Writer: httptest.NewRecorder(), + } + r := httptest.NewRequest("GET", "/not-important-here", nil) + w := httptest.NewRecorder() + + getSSLClietnDN(w, r, &h) + + if res := w.Result(); res.StatusCode != http.StatusNotFound { + t.Errorf("Status code mismatch. Expected: %d, got: %d", http.StatusNotFound, res.StatusCode) + } +} + +func TestGetSSLClientDN200sOnHappyPath(t *testing.T) { + h := model.Handler{ + Request: httptest.NewRequest("POST", "https://www.foo.bar:8080/", nil), + Writer: httptest.NewRecorder(), + } + h.Request.TLS.VerifiedChains = [][]*x509.Certificate{{new(x509.Certificate)}} + r := httptest.NewRequest("GET", "/not-important-here", nil) + w := httptest.NewRecorder() + + getSSLClietnDN(w, r, &h) + + if res := w.Result(); res.StatusCode != http.StatusOK { + t.Errorf("Status code mismatch. Expected: %d, got: %d", http.StatusOK, res.StatusCode) + } +} + +func TestGetSSLClientDNSetsOctectStreamContentType(t *testing.T) { + h := model.Handler{ + Request: httptest.NewRequest("POST", "https://www.foo.bar:8080/", nil), + Writer: httptest.NewRecorder(), + } + h.Request.TLS.VerifiedChains = [][]*x509.Certificate{{new(x509.Certificate)}} + r := httptest.NewRequest("GET", "/not-important-here", nil) + w := httptest.NewRecorder() + + getSSLClietnDN(w, r, &h) + + res := w.Result() + if v := res.Header.Get("Content-Type"); v != "application/octet-stream" { + t.Errorf("Status code mismatch. Expected: %q, got: %q", "application/octet-stream", v) + } +} + +func mockAuthenticateClient(tls *tls.ConnectionState) error { + fileData, err := ioutil.ReadFile("./testdata/client_chain.crt") + if err != nil { + return fmt.Errorf("Error loading certificates file: %v", err) + } + + asn1Data, _ := pem.Decode(fileData) + certs, err := x509.ParseCertificates(asn1Data.Bytes) + if err != nil { + return fmt.Errorf("Error parsing certificates data: %v", err) + } + + tls.VerifiedChains = [][]*x509.Certificate{certs} + tls.PeerCertificates = []*x509.Certificate{tls.VerifiedChains[0][0]} + + return nil +} + +func TestGetSSLClientDNReturnsCorrectDN(t *testing.T) { + h := model.Handler{ + Request: httptest.NewRequest("POST", "https://www.foo.bar:8080/", nil), + Writer: httptest.NewRecorder(), + } + if err := mockAuthenticateClient(h.Request.TLS); err != nil { + t.Error(err) + } + r := httptest.NewRequest("GET", "/not-important-here", nil) + w := httptest.NewRecorder() + + getSSLClietnDN(w, r, &h) + + res := w.Result() + + if body, _ := ioutil.ReadAll(res.Body); string(body) != h.Request.TLS.VerifiedChains[0][0].Subject.CommonName { + t.Errorf("Body mismatch. Expected: %q, got: %q", h.Request.TLS.VerifiedChains[0][0].Subject.CommonName, string(body)) + } +} func TestGetRouteId200sOnHappyPath(t *testing.T) { h := model.Handler{ diff --git a/internal/server/data/server.go b/internal/server/data/server.go index 791cdc0..8235ed8 100644 --- a/internal/server/data/server.go +++ b/internal/server/data/server.go @@ -72,7 +72,10 @@ func Run(bindAddr string, wg *sync.WaitGroup) { {"/handlers/{handlerID}/request/body", "GET", getRequestBody}, // route - //{"/handlers/{handlerID}/route/id", "GET", getRouteId}, + {"/handlers/{handlerID}/route/id", "GET", getRouteId}, + + // SSL stuff + {"/handlers/{handlerID}/ssl/client/i/dn", "GET", getSSLClietnDN}, // response {"/handlers/{handlerID}/response/status", "PUT", lockResponseWriter(setResponseStatus)}, diff --git a/internal/server/data/testdata/client_chain.crt b/internal/server/data/testdata/client_chain.crt new file mode 100644 index 0000000..4b8700f --- /dev/null +++ b/internal/server/data/testdata/client_chain.crt @@ -0,0 +1,46 @@ +-----BEGIN CERTIFICATE----- +MIIDdTCCAl0CAQIwDQYJKoZIhvcNAQELBQAwgZ8xCzAJBgNVBAYTAkVTMQ8wDQYD +VQQIDAZNYWRyaWQxDzANBgNVBAcMBk1hZHJpZDENMAsGA1UECgwEQkJWQTEYMBYG +A1UECwwPSW5ub3ZhdGlvbiBMYWJzMR0wGwYDVQQDDBRTZWN1cml0eS1DQS5iYnZh +LmNvbTEmMCQGCSqGSIb3DQEJARYXc2VjdXJpdHkuZ3JvdXBAYmJ2YS5jb20wHhcN +MjAwMTIzMTQwODUxWhcNMjEwMTIyMTQwODUxWjBhMQswCQYDVQQGEwJFUzEPMA0G +A1UECAwGTWFkcmlkMQ0wCwYDVQQKDARCQlZBMRgwFgYDVQQLDA9Jbm5vdmF0aW9u +IExhYnMxGDAWBgNVBAMMD0thcG93ISBjbGllbnQgMTCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBAJKXoqOe0S1i8c0bDLGsvibSpDmWkb/2oXn4qn8XtlLF +PSY69qeqkeLZov0nVV6zenag9Vh99uy7M4kw/pFqYv1eViDvV6I0wyKEzXNyeQmL +O11YUWfP38T+Usw0JY0Pau+ewSQkurtHmGRWC5fAgPxiyi03hD3V3eHPR60V5yE6 +1wYoLqz6xJ7nkXVyVLdg6wekrMkpos3ciA3Roco4m5fbXbVGYrx8E97byT9yyKzI +kvFjQ0T4+67dPuWW+juoD0lOwfNu1WtY4PPnkUuzjUftKzD1t/a4zsLcFLafuuVR +f4qb21o2pqDOWxMMZGrceS5uEF0nI1wtdw+XMtFHdFcCAwEAATANBgkqhkiG9w0B +AQsFAAOCAQEAsnPW8pCIiejBwjQ4TTNPo5wiRNOib69ANj2lHE1gidO8HA29/ssF +U7jbcxCQf0/flv+JddnSJzmeFhrt15CL6nOZ1whSqVA1W1dAno0RYNiPUILofq50 +zKNUVF+eYz24nksdI87d9j1Zri2H91p+gA1pBnIxBE8zgXZ5+u7FUrA41HOuVyAy +55EwUDloVg4WBeddb8Y/mPgXNHS7ZB0Z13+bLHeSkSWWV3Gw1OtLHJEv/j+/9K5O +KoJCyO4xSkKP7/nYYKCed4grIfOAu7iHqN/Ok9yuAOm0tbgwKmzw9EGga82YCH0M +jfd8wfVCqiZvW9SUa11fM/Np9/+04QtlNA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEITCCAwmgAwIBAgIUMAqlEXi1gcpy97bWApqtBwgqEvgwDQYJKoZIhvcNAQEL +BQAwgZ8xCzAJBgNVBAYTAkVTMQ8wDQYDVQQIDAZNYWRyaWQxDzANBgNVBAcMBk1h +ZHJpZDENMAsGA1UECgwEQkJWQTEYMBYGA1UECwwPSW5ub3ZhdGlvbiBMYWJzMR0w +GwYDVQQDDBRTZWN1cml0eS1DQS5iYnZhLmNvbTEmMCQGCSqGSIb3DQEJARYXc2Vj +dXJpdHkuZ3JvdXBAYmJ2YS5jb20wHhcNMjAwMTIyMTcxNzUwWhcNMjAwMjIxMTcx +NzUwWjCBnzELMAkGA1UEBhMCRVMxDzANBgNVBAgMBk1hZHJpZDEPMA0GA1UEBwwG +TWFkcmlkMQ0wCwYDVQQKDARCQlZBMRgwFgYDVQQLDA9Jbm5vdmF0aW9uIExhYnMx +HTAbBgNVBAMMFFNlY3VyaXR5LUNBLmJidmEuY29tMSYwJAYJKoZIhvcNAQkBFhdz +ZWN1cml0eS5ncm91cEBiYnZhLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAMM8xmTpUg2PXV6zPohGOKLiXHP3nQMfkoYLRwFYcKq0WnYmaKmIK5T0 +gCLmiQMCNv/NV8+aIkd2HuTPBnobFLbyUCN9yf2Gj81wxeRydBwbjaj0dpB1Jx9W +5OdMBFxGIKmnqVN/z784Ma9cj+tv0t5LYpWIxnEgnBaiuMkQnwJFyv6aL1VNRmW7 +zFMAqOiMishMKb/0UaSW53ZBFjCqmhquZ7CZYLsaB+mMDv1fOXf8jSX/WrcCCkvQ +HX04/9HxNVIe3a0Zl8CPfyUOd9njVl1VRDgjljUyMeMM4zo31enpwWOuhpfI6jU5 +AB7If6xufHyN8FCvKpDy9Z9Sp5Ww1o8CAwEAAaNTMFEwHQYDVR0OBBYEFOfc0yly +jUPuoy54Ods9KKt5CITsMB8GA1UdIwQYMBaAFOfc0ylyjUPuoy54Ods9KKt5CITs +MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBALvzf14HF78rowNA +XuczbkgKLbNJam0YC3ZxoB/pxcwfSZCf4E7+FAWKfmwrNqZv2PweOvDfP4Rx4T1x +VLWDr0qtcsol7gOPga8HD/1zTgK096rYs5pCxQabgIpmHhzDUnjBrQyds8U4sakP +3xzuy/eZ/ozDzCGZn8HzqYHDTcEfypMSdZUUDgN4vVE3Il3AZRSEG9X/Ov4W8tLF +dofY/JDVObXV0DAms9xcux8BfAslh8NNytP3q+uneIeuJT/eRQu+Z5GRB+mbL8X6 +DmXeSSkMiVeFgD1qg9VZVSBsihBpWpJakcxvXOOj5fY2t5ovW6TR2jDjW5XjHs82 +idlvJUw= +-----END CERTIFICATE-----