Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add TLS connection info to response object #322

Merged
merged 4 commits into from Oct 24, 2017
Merged

Conversation

robingustafsson
Copy link
Member

@robingustafsson robingustafsson commented Sep 25, 2017

This PR adds TLS version, cipher suite and OCSP stapled response information to each HTTP response object, implementing #298.

Example usage:

import http from "k6/http";
import { check } from "k6";

export let options = {
    tlsCipherSuites: [
        "TLS_RSA_WITH_RC4_128_SHA",
        "TLS_RSA_WITH_AES_128_GCM_SHA256",
    ],
    tlsVersion: {
        min: "ssl3.0",
        max: "tls1.2"
    }
};

export default function() {
    let res = http.get("https://sha256.badssl.com");
    check(res, {
        "is TLSv1.2": (r) => r.tls_version === "tls1.2",
        "is sha256 cipher suite": (r) => r.tls_cipher_suite === "TLS_RSA_WITH_AES_128_GCM_SHA256"
    });
};

and

import http from "k6/http";
import { check } from "k6";

export default function() {
    let res = http.get("https://stackoverflow.com");
    check(res, {
        "is OCSP response good": (r) => r.ocsp.stapled_response.status === "good"
    });
};

The OCSP stapled_response object also contains the following properties:

  • ocsp.stapled_response.revocation_reason: a string
  • ocsp.stapled_response.produced_at: a date string
  • ocsp.stapled_response.this_update: a date string
  • ocsp.stapled_response.next_update: a date string
  • ocsp.stapled_response.revoked_at: a date string

@robingustafsson
Copy link
Member Author

@liclac
Copy link
Contributor

liclac commented Oct 4, 2017

I want to make a structural change here, because for the first time in k6' history, we have a use for constants.

Making this easy-to-read string comparisons (ocsp.status === "good") sounds good in theory, until someone typo's it as "god" and suddenly we're refusing to trust any authority short of a holy scripture. And while that's a fairly easy one to catch, what about "tls1.2" vs "TLS1.2"?

The change I'm proposing is:

  1. Change the module definition from simply type HTTP struct{}, to:

    type HTTP struct{
        SSL_3_0, TLS_1_0, TLS_1_1, TLS_1_2 string
    }
    
    func New() *HTTP {
        return &HTTP{
            SSL_3_0: "ssl3.0",
            // ...
        }
    }
  2. Change js/modules/index.go from &http.HTTP{} to http.New(), and for consistency, also do this for all modules + their unit tests.

The actual naming of the constants is up for debate, and I think OCSP status may be better implemented as a tristate bool (*bool, true/false/null).

@@ -34,9 +35,12 @@ import (
"sync"
"time"

"golang.org/x/crypto/ocsp"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please merge this into the import block below.

}

type OCSP struct {
StapledResponse OCSPStapledResponse
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this indirection really needed...?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably not, maybe it should just be res.ocsp_response on the JS side?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or maybe even just res.ocsp?

@@ -240,6 +257,60 @@ func (*HTTP) request(ctx context.Context, rt *goja.Runtime, state *common.State,
tags["status"] = strconv.Itoa(resp.Status)
tags["proto"] = resp.Proto

if res.TLS != nil {
tlsState := res.TLS
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can shorten this down to if tlsState := res.TLS; tlsState != nil {.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 typical Go noob mistake 😄

resp.TLSVersion = "tls1.2"
}
resp.TLSCipherSuite = lib.SupportedTLSCipherSuitesToString[tlsState.CipherSuite]
resp.OCSP.StapledResponse = OCSPStapledResponse{}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is redundant.

@@ -240,6 +257,60 @@ func (*HTTP) request(ctx context.Context, rt *goja.Runtime, state *common.State,
tags["status"] = strconv.Itoa(resp.Status)
tags["proto"] = resp.Proto

if res.TLS != nil {
tlsState := res.TLS
switch tlsState.Version {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make this a helper function, then assign resp.OCSP.StapledResponse to a fully initialized object. These switches are real lengthy.

resp.OCSP.StapledResponse.ProducedAt = ocspRes.ProducedAt.String()
resp.OCSP.StapledResponse.ThisUpdate = ocspRes.ThisUpdate.String()
resp.OCSP.StapledResponse.NextUpdate = ocspRes.NextUpdate.String()
resp.OCSP.StapledResponse.RevokedAt = ocspRes.RevokedAt.String()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be in a format we can compare on the JS side... UNIX timestamps?

(I'm also kind of questioning what purpose it will serve for a load test script; I suppose it could be useful for logging, but not much more.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, should we use JS Date objects or Epoch milliseconds (to compare with Date.now() and Date.getTime()) when exposing to JS? Had the same question in the cookie PR.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just in the interest of performance, I think maybe giving a UNIX timestamp would be better than instancing whole date objects every time. The JS-native way seems to be to deal with milliseconds for them, so then you can just do new Date(revokedAt).

_, err := common.RunString(rt, `http.get("https://expired.badssl.com/");`)
assert.EqualError(t, err, "GoError: Get https://expired.badssl.com/: x509: certificate has expired or is not yet valid")
})
t.Run("tls10", func(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These versions are nigh identical and would do better templated than copypasted.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, will look into that.

@liclac
Copy link
Contributor

liclac commented Oct 4, 2017

I'd also like TLS version and OCSP status to be added as tags to responses.

@robingustafsson
Copy link
Member Author

@liclac Haha, ocsp.status === "god" is of course the status when you've successfully infiltrated a CA with a HTTP request 😄

Agree on the use of constants for TLS version and cipher suites. I'll change the OCSP status to use constants as well. IMHO that's more clear when reading than a tristate.

@robingustafsson
Copy link
Member Author

@liclac Requested changes have now been done, please have another look.

case tls.VersionTLS12:
res.TLSVersion = h.TLS_1_2
}
res.TLSCipherSuite = lib.SupportedTLSCipherSuitesToString[tlsState.CipherSuite]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Newline, please.

res.TLSVersion = h.TLS_1_2
}
res.TLSCipherSuite = lib.SupportedTLSCipherSuitesToString[tlsState.CipherSuite]
ocspStapledRes := OCSP{Status: h.OCSP_STATUS_UNKNOWN}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Newline, please.

func (res *HTTPResponse) setTLSInfo(h *HTTP, tlsState *tls.ConnectionState) {
switch tlsState.Version {
case tls.VersionSSL30:
res.TLSVersion = h.SSL_3_0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These constants should honestly be global consts, passing in the HTTP object just to read them feels silly.

RevocationReason string
Status string
}

type HTTPResponseTimings struct {
Duration, Blocked, LookingUp, Connecting, Sending, Waiting, Receiving float64
}

type HTTPResponse struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This struct is honestly getting big enough that I'd split it off into a response.go file.

@@ -240,6 +348,12 @@ func (*HTTP) request(ctx context.Context, rt *goja.Runtime, state *common.State,
tags["status"] = strconv.Itoa(resp.Status)
tags["proto"] = resp.Proto

if tlsState := res.TLS; res.TLS != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tlsState is longer than res.TLS. As far as shorthands go, that's pretty lousy.

@robingustafsson
Copy link
Member Author

@liclac Requested changes have been made.

@liclac liclac merged commit c849005 into master Oct 24, 2017
@liclac liclac deleted the feature/tls-resp-info branch October 24, 2017 14:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants