danielgtaylor / restish

Restish is a CLI for interacting with REST-ish HTTP APIs with some nice features built-in
https://rest.sh/
MIT License
717 stars 69 forks source link

Make it possible to use a client certificate stored in hardware #246

Closed jeffallen closed 1 month ago

jeffallen commented 3 months ago

This has been tested with a Yubikey and the OpenSC PKCS#11 interface on Linux.

jeffallen commented 3 months ago

Hello @danielgtaylor . This is something we need at Exoscale, and we wanted to show it to you and see what you think. The fact that it pulls in code that needs cgo is unfortunate but more or less unavoidable for how PKCS#11 plugins work.

If that's a deal breaker we'll see what we can do about it. One possibility is to put the pkcs11 stuff into a separate binary (in a separate repo) called "pkcs11-interface", which would talk to restish (and other systems) via i/o on stdio.

I plan to send in some kind of fix for the Windows build failure. The proposed pkcs11-interface would fix Windows as well.

danielgtaylor commented 3 months ago

@jeffallen thanks for the PR. I have to admit I'm not familiar with this plugin system and how it works. Given most users install from homebrew or binaries I'm not too worried about adding cgo, but I do like the idea of a plugin similar to how we can currently shell out for custom auth scripts. I'll have to read up on this a bit and figure out how I can test it out.

codecov[bot] commented 3 months ago

Codecov Report

Attention: Patch coverage is 0% with 33 lines in your changes are missing coverage. Please review.

Project coverage is 76.18%. Comparing base (d16bdd7) to head (4875cdb).

:exclamation: Current head 4875cdb differs from pull request most recent head 4c2b845. Consider uploading reports for the commit 4c2b845 to get more accurate results

Additional details and impacted files [![Impacted file tree graph](https://app.codecov.io/gh/danielgtaylor/restish/pull/246/graphs/tree.svg?width=650&height=150&src=pr&token=1BMagYibk9&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Daniel+G.+Taylor)](https://app.codecov.io/gh/danielgtaylor/restish/pull/246?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Daniel+G.+Taylor) ```diff @@ Coverage Diff @@ ## main #246 +/- ## ========================================== - Coverage 76.86% 76.18% -0.69% ========================================== Files 26 26 Lines 3679 3712 +33 ========================================== Hits 2828 2828 - Misses 643 675 +32 - Partials 208 209 +1 ``` | [Files](https://app.codecov.io/gh/danielgtaylor/restish/pull/246?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Daniel+G.+Taylor) | Coverage Δ | | |---|---|---| | [cli/apiconfig.go](https://app.codecov.io/gh/danielgtaylor/restish/pull/246?src=pr&el=tree&filepath=cli%2Fapiconfig.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Daniel+G.+Taylor#diff-Y2xpL2FwaWNvbmZpZy5nbw==) | `83.87% <ø> (ø)` | | | [cli/request.go](https://app.codecov.io/gh/danielgtaylor/restish/pull/246?src=pr&el=tree&filepath=cli%2Frequest.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Daniel+G.+Taylor#diff-Y2xpL3JlcXVlc3QuZ28=) | `60.53% <0.00%> (-7.51%)` | :arrow_down: | ------ [Continue to review full report in Codecov by Sentry](https://app.codecov.io/gh/danielgtaylor/restish/pull/246?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Daniel+G.+Taylor). > **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Daniel+G.+Taylor) > `Δ = absolute (impact)`, `ø = not affected`, `? = missing data` > Powered by [Codecov](https://app.codecov.io/gh/danielgtaylor/restish/pull/246?dropdown=coverage&src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Daniel+G.+Taylor). Last update [d16bdd7...4c2b845](https://app.codecov.io/gh/danielgtaylor/restish/pull/246?dropdown=coverage&src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Daniel+G.+Taylor). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Daniel+G.+Taylor).
jeffallen commented 3 months ago

FYI, the upgrade to Go 1.22.2 fixed the Windows build error: https://github.com/exoscale/restish/actions/runs/8784557557

danielgtaylor commented 1 month ago

@jeffallen sorry for the delay! This was not the easiest change to test! I finally had a chance to get this working locally and test it out. Here are the steps for anyone who is interested (and probably my future self if we're being honest):

  1. Get a hardware security dongle like a Yubikey (thank you to Exoscale for sending me one). Install & open the Yubikey Manager. Generate a certificate via Applications → PIV → Certificates → Authentication (Slot 9a) → Generate, then export & save as client-pub.pem. Issuer and subject can just be test. The default pin is 123456.
  2. Install OpenSC: https://github.com/OpenSC/OpenSC/wiki/macOS-Quick-Start. This will provide the required library to talk to the Yubikey to sign requests with the private on-device key.
  3. Run OpenSSL to generate a self-signed server certificate for SSL. We don't really care about this but it's required to run the server via TLS.
    $ openssl req -newkey rsa:2048 \
     -new -nodes -x509 \
     -days 3650 \
     -out cert.pem \
     -keyout key.pem \
     -subj "/C=US/ST=California/L=Mountain View/O=Your Organization/OU=Your Unit/CN=localhost"
  4. Write a simple Go server with TLS that requires a client cert. Something like this saved in main.go:

    package main
    
    import (
        "crypto/tls"
        "crypto/x509"
        "io"
        "io/ioutil"
        "log"
        "net/http"
    )
    
    func helloHandler(w http.ResponseWriter, r *http.Request) {
        // Write "Hello, world!" to the response body
        io.WriteString(w, "Hello, world!\n")
    }
    
    func main() {
        // Set up a /hello resource handler
        http.HandleFunc("/hello", helloHandler)
    
        // Create a CA certificate pool and add the client's public key to it.
        caCert, err := ioutil.ReadFile("client-pub.pem")
        if err != nil {
            log.Fatal(err)
        }
        caCertPool := x509.NewCertPool()
        caCertPool.AppendCertsFromPEM(caCert)
    
        // Create the TLS Config with the CA pool and enable Client certificate validation
        tlsConfig := &tls.Config{
            ClientCAs:  caCertPool,
            ClientAuth: tls.RequireAndVerifyClientCert,
        }
        tlsConfig.BuildNameToCertificate()
    
        // Create a Server instance to listen on port 8443 with the TLS config
        server := &http.Server{
            Addr:      ":8443",
            TLSConfig: tlsConfig,
        }
    
        // Listen to HTTPS connections with the server certificate and wait
        log.Fatal(server.ListenAndServeTLS("cert.pem", "key.pem"))
    }
  5. Run the server: go run .
  6. Set up a config in Restish for testing so that it loads the opensc-pkcs11.so (use restish api edit):
    "local8443": {
        "base": "https://localhost:8443",
        "profiles": {
          "default": {}
        },
        "tls": {
          "ca_cert": "",
          "cert": "",
          "insecure": false,
          "key": "",
          "pkcs11": {
            "path": "/Library/OpenSC/lib/opensc-pkcs11.so",
            "label": "test"
          }
        }
      },
  7. Check out this PR (or a future release of Restish), then run the following. Note we'll use --rsh-insecure to ignore the fact that the server's certificate is self-signed, otherwise Restish would refuse to continue because it doesn't trust the server. This option has no impact on client certs.
    $ cd src/restish
    $ gh pr checkout 246
    $ go install
    $ export PKCS11_PIN=123456
    $ restish --rsh-insecure https://localhost:8443/hello

Here are the results:

  1. Without any Restish config (i.e. no PKCS11 client cert set up)
    ERROR: Caught error: Get "https://localhost:8443/hello": remote error: tls: certificate required
  2. With the config, but without the Yubikey inserted:
    ERROR: Caught error: Get "https://localhost:8443/hello": could not find PKCS#11 token
  3. With the config, with the Yubikey:

    WARN: Disabling TLS security checks
    HTTP/2.0 200 OK
    Content-Length: 14
    Content-Type: text/plain; charset=utf-8
    Date: Wed, 29 May 2024 05:31:56 GMT
    
    Hello, world!

Success! Thanks for the work on this. :+1: