roadrunner-server / roadrunner

🤯 High-performance PHP application server, process manager written in Go and powered with plugins
https://docs.roadrunner.dev
MIT License
7.86k stars 408 forks source link

[💡 FEATURE REQUEST]: Expose client certificate to PHP #1324

Open dwgebler opened 1 year ago

dwgebler commented 1 year ago

Plugin

HTTP

I have an idea!

mTLS works great, would be even better if this plugin and the PHP worker library could expose variables representing the parsed client certificate in PHP's $_SERVER superglobal or environment or PSR ServerRequest, in the same way for example on Apache you'd get:

$_SERVER['HTTPS'] = "on" $_SERVER['SSL_CLIENT_VERIFY'] = "SUCCESS" $_SERVER['SSL_CLIENT_S_DN'] = <DN string>

rustatian commented 1 year ago

Hey @dwgebler 👋🏻 Thanks for the proposal. I'll discuss it with our PHP team 👍🏻

dwgebler commented 1 year ago

Thanks @rustatian this would be particularly useful for

a) a service which may run on RR via either HTTP or HTTPS and you'd like to be able to know which method was used by the client

b) a service is running via HTTPS with mTLS but you'd like to be able to distinguish between clients based on the DN of the certificate issued to them, to identify the user

rustatian commented 1 year ago

Yeah, thanks for the clarification. I'll discuss that with the PHP team on Monday 😃. This is, for sure, entirely not a problem to pass some envs to the PHP process (with the parsed certs data), I just need to sync with the PHP team on that (since I'm not a PHP dev 😢).

rustatian commented 1 year ago

specs:

  1. https://www.cryptosys.net/pki/manpki/pki_distnames.html
  2. https://www.alvestrand.no/objectid/2.5.4.html
  3. https://httpd.apache.org/docs/2.4/mod/mod_ssl.html
dwgebler commented 1 year ago

As a quick and dirty draft, here are the changes I made in roadrunner-http (Go) and roadrunner-http (PHP) to get what I wanted:

In HTTP handler getReq:

    if r.TLS != nil {
        req.IsHttps = true
        // Get a complete JSON representation of the entire certificate, why not?
        cert, err := json.Marshal(r.TLS.PeerCertificates[0])
        if err != nil {
            h.log.Error("failed to marshal certificate", zap.Error(err))
        } else {
            req.Certificate = string(cert)
            req.CertificateSubject = r.TLS.PeerCertificates[0].Subject.String()
        }
    }

And just modified request to include these in the JSON payload which the PHP worker receives.

    Certificate string `json:"certificate"`
    CertificateSubject string `json:"certificateSubject"`
    IsHttps bool `json:"HTTPS"`

Then in PHP roadrunner-http package:

        $request->tlsParams['HTTPS'] = $context['HTTPS'] ?? false;

        if (!empty($context['certificate'])) {
            $request->tlsParams['ssl_client_verify'] = 'SUCCESS';
            $request->tlsParams['ssl_client_certificate'] = json_decode($context['certificate'], true);
            $request->tlsParams['ssl_client_subject'] = $context['certificateSubject'] ?? '';
        }

In PSR7Worker::configureServer:

        $server['HTTPS'] = $request->tlsParams['HTTPS'];
        $server['SSL_CLIENT_VERIFY'] = $request->tlsParams['ssl_client_verify'];
        $server['SSL_CLIENT_CERT'] = $request->tlsParams['ssl_client_certificate'];
        $server['SSL_CLIENT_SUBJECT'] = $request->tlsParams['ssl_client_subject'];
rustatian commented 1 year ago

Yeah, nice 😃 It should be in the separate middleware generally. I'm also not a big fan of the json'ing everything, but as the POC looks nice, thanks.

If you need this functionality ASAP, you may also fork the HTTP plugin and build your custom RR with the Velox. You may have a look at the tutorial here: https://youtu.be/h5PPvc_YOtg

dwgebler commented 1 year ago

Yeah, nice 😃 It should be in the separate middleware generally. I'm also not a big fan of the json'ing everything, but as the POC looks nice, thanks.

If you need this functionality ASAP, you may also fork the HTTP plugin and build your custom RR with the Velox. You may have a look at the tutorial here: https://youtu.be/h5PPvc_YOtg

Cheers for that, I've re-written what I need as a custom middleware plugin which adds a flag for whether the request was HTTP/HTTPS and the client certificate data if present through the existing PSR-7 attributes on the ServerRequest, so very minimal change. Compiled RR binary with the plugin and enabled it in my application .rr.yaml and it all works a treat :) obviously would be great to have this feature included in the official RR HTTP package as a default middleware so anyone else needing similar functionality can just have it out the box. Great learning experience for me though, this was my first time writing any Go.

RoadRunner is fantastic, by the way! Got an API which I built with Symfony components which I was running on a regular Apache/PHP-FPM stack with mTLS, where the username is looked up by the certificate CN. On this stack a single API request took about 80ms on average, the same API translated to Roadrunner (and still using Symfony components with PSR-7 bridge) with my plugin is completing the same request in an average 1.4ms! That is a phenomenal improvement, I love it.

rustatian commented 1 year ago

obviously would be great to have this feature included in the official RR HTTP package as a default middleware so anyone else needing similar functionality can just have it out the box.

Nice, yeah, I'll create an smt like a mod_tls middleware. We just need a little bit more upvotes on your proposal 😃

Great learning experience for me though, this was my first time writing any Go.

Excellent, my idea about plugins was precisely about what you did. Anyone can create a plugin for the RR, even with a small Go experience, and then compile its RR binary for any OS.

On this stack a single API request took about 80ms on average, the same API translated to Roadrunner (and still using Symfony components with PSR-7 bridge) with my plugin is completing the same request in an average 1.4ms! That is a phenomenal improvement, I love it.

Wow, that's impressive 👍🏻 Very glad to hear that 😃 Enjoy RR ❤️

You may also join our discord server and ping our PHP guys or me if you have questions: https://discord.gg/TFeEmCs

We also have our framework: https://github.com/spiral/framework 😃