diff --git a/examples/README.md b/examples/README.md index dcb5229c..42df4ce5 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,6 +1,137 @@ -# Example +# Examples -# Bootstrap Client & Server +## Basic client usage + +The basic-client example shows the use of the most of the functioanlity of the +`ca.Client`, those methods works as an SDK for integrating other services with +the Certificate Authority (CA). + +In [basic-client/client.go](/examples/basic-client/client.go) we first can see +the initialization of the client: + +```go +client, err := ca.NewClient("https://localhost:9000", ca.WithRootSHA256("84a033e84196f73bd593fad7a63e509e57fd982f02084359c4e8c5c864efc27d")) +``` + +The previous code uses the CA address and the root certificate fingerprint. +The CA url will be present in the token, and the root fingerprint can be present +too if the `--root root_ca.crt` option is use in the creation of the token. If +this is the case is simpler to rely in the token and use just: + +```go +client, err := ca.Bootstrap(token) +``` + +After the initialization there're examples of all the client methods, they are +just a convenient way to use the CA API endpoints. The first method `Health` +returns the status of the CA server, on the first implementation if the server +is up it will return just ok. + +```go +health, err := client.Health() +// Health is a struct created from the JSON response {"status": "ok"} +``` + +The next method `Root` is used to get and verify the root certificate. We will +pass a finger print, it will download the root certificate from the CA and it +will make sure that the fingerprint matches. This method uses an insecure HTTP +client as it might be used in the initialization of the client, but the response +is considered secure because we have compared against the given digest. + +```go +root, err := client.Root("84a033e84196f73bd593fad7a63e509e57fd982f02084359c4e8c5c864efc27d") +``` + +After Root we have the most important method `Sign`, this is used to sign a +Certificate Signing Request that we provide. To secure this request we use +the one-time-token. You can build your own certificate request and add it in +the `*api.SignRequest`, but the ca package contains a method that will create a +secure random key, and create the CSR based on the information in the token. + +```go +// Create a CSR from token and return the sign request, the private key and an +// error if something failed. +req, pk, err := ca.CreateSignRequest(token) +if err != nil { ... } + +// Do the sign request and return the signed certificate +sign, err := client.Sign(req) +if err != nil { ... } +``` + +To renew the certificate we can use the `Renew` method, the certificate renewal +relies on a mTLS connection with a previous certificate, so we will need to pass +a transport with the previous certificate. + +```go +// Get a cancelable context to stop the renewal goroutines and timers. +ctx, cancel := context.WithCancel(context.Background()) +defer cancel() +// Create a transport from with the sign response and the private key. +tr, err := client.Transport(ctx, sign, pk) +if err != nil { ... } +// Renew the certificate and get the new ones. +// The return type are equivalent to ones in the Sign method. +renew, err := client.Renew(tr) +if err != nil { ... } +``` + +All the previous methods map with one endpoint in the CA API, but the API +provides a couple more that are used for creating the tokens. For those we have +a couple of methods, one that returns a list of provisioners and one that +returns the encrypted key of one provisioner. + +```go +// Without options it will return the first 20 provisioners +provisioners, err := client.Provisioners() +// We can also set a limit up to 100 +provisioners, err := client.Provisioners(ca.WithProvisionerLimit(100)) +// With a pagination cursor +provisioners, err := client.Provisioners(ca.WithProvisionerCursor("1f18c1ecffe54770e9107ce7b39b39735")) +// Or combining both +provisioners, err := client.Provisioners( + ca.WithProvisionerCursor("1f18c1ecffe54770e9107ce7b39b39735"), + ca.WithProvisionerLimit(100), +) + +// Return the encrypted key of one of the returned provisioners. The key +// returned is an encrypted JWE with the private key used to sign tokens. +key, err := client.ProvisionerKey("DmAtZt2EhmZr_iTJJ387fr4Md2NbzMXGdXQNW1UWPXk") +``` + +The example shows also the use of some helper methods used to get configured +tls.Config objects that can be injected in servers and clients. These methods, +are also configured to auto-renew the certificate once two thirds of the +duration of the certificate has passed, approximately. + +```go +// Get a cancelable context to stop the renewal goroutines and timers. +ctx, cancel := context.WithCancel(context.Background()) +defer cancel() +// Get tls.Config for a server +tlsConfig, err := client.GetClientTLSConfig(ctx, sign, pk) +// Get tls.Config for a client +tlsConfig, err := client.GetClientTLSConfig(ctx, sign, pk) +// Get an http.Transport for a client, this can be used as a http.RoundTripper +// in an http.Client +tr, err := client.Transport(ctx, sign, pk) +``` + +To run the example you need to start the certificate authority: + +``` +certificates $ bin/step-ca examples/pki/config/ca.json +2018/11/02 18:29:25 Serving HTTPS on :9000 ... +``` + +And just run the client.go with a new token: +``` +certificates $ export STEPPATH=examples/pki +certificates $ export STEP_CA_URL=https://localhost:9000 +certificates $ go run examples/basic-client/client.go $(step ca new-token client.smallstep.com)) +``` + +## Bootstrap Client & Server On this example we are going to see the Certificate Authority running, as well as a simple Server using TLS and a simple client doing TLS requests to the @@ -18,7 +149,7 @@ certificates $ bin/step-ca examples/pki/config/ca.json We will start the server and we will type `password` when step asks for the provisioner password: ``` -certificates $ export STEPPATH=examples/pki +certificates $ export STEPPATH=examples/pki certificates $ export STEP_CA_URL=https://localhost:9000 certificates $ go run examples/bootstrap-server/server.go $(step ca new-token localhost)) ✔ Key ID: DmAtZt2EhmZr_iTJJ387fr4Md2NbzMXGdXQNW1UWPXk (mariano@smallstep.com) @@ -56,7 +187,7 @@ detected a TLS client configuration. But if we the client with the certificate name Mike we'll see: ``` -certificates $ export STEPPATH=examples/pki +certificates $ export STEPPATH=examples/pki certificates $ export STEP_CA_URL=https://localhost:9000 certificates $ go run examples/bootstrap-client/client.go $(step ca new-token Mike) ✔ Key ID: DmAtZt2EhmZr_iTJJ387fr4Md2NbzMXGdXQNW1UWPXk (mariano@smallstep.com) diff --git a/examples/basic-client/client.go b/examples/basic-client/client.go new file mode 100644 index 00000000..4ea4d793 --- /dev/null +++ b/examples/basic-client/client.go @@ -0,0 +1,152 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "net" + "net/http" + "os" + "time" + + "github.com/smallstep/certificates/ca" +) + +func printResponse(name string, v interface{}) { + b, err := json.MarshalIndent(v, "", " ") + if err != nil { + panic(err) + } + fmt.Printf("%s response:\n%s\n\n", name, b) +} + +func main() { + if len(os.Args) != 2 { + fmt.Fprintf(os.Stderr, "Usage: %s \n", os.Args[0]) + os.Exit(1) + } + + token := os.Args[1] + + // To create the client using ca.NewClient we need: + // * The CA address "https://localhost:9000" + // * The root certificate fingerprint + // 84a033e84196f73bd593fad7a63e509e57fd982f02084359c4e8c5c864efc27d to get + // the root fingerprint we can use `step certificate fingerprint root_ca.crt` + client, err := ca.NewClient("https://localhost:9000", ca.WithRootSHA256("84a033e84196f73bd593fad7a63e509e57fd982f02084359c4e8c5c864efc27d")) + if err != nil { + panic(err) + } + + // Other ways to initialize the client would be: + // * With the Bootstrap functionality (recommended): + // client, err := ca.Bootstrap(token) + // * Using the root certificate instead of the fingerprint: + // client, err := ca.NewClient("https://localhost:9000", ca.WithRootFile("../pki/secrets/root_ca.crt")) + + // Get the health of the CA + health, err := client.Health() + if err != nil { + panic(err) + } + printResponse("Health", health) + + // Get and verify a root CA + root, err := client.Root("84a033e84196f73bd593fad7a63e509e57fd982f02084359c4e8c5c864efc27d") + if err != nil { + panic(err) + } + printResponse("Root", root) + + // We can use ca.CreateSignRequest to generate a new sign request with a + // randomly generated key. + req, pk, err := ca.CreateSignRequest(token) + if err != nil { + panic(err) + } + sign, err := client.Sign(req) + if err != nil { + panic(err) + } + printResponse("Sign", sign) + + // Renew a certificate with a transport that contains the previous + // certificate. We should created a context that allows us to finish the + // renewal goroutine.∑ + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() // Finish the renewal goroutine + tr, err := client.Transport(ctx, sign, pk) + if err != nil { + panic(err) + } + renew, err := client.Renew(tr) + if err != nil { + panic(err) + } + printResponse("Renew", renew) + + // Get tls.Config for a server + ctxServer, cancelServer := context.WithCancel(context.Background()) + defer cancelServer() + tlsConfig, err := client.GetServerTLSConfig(ctxServer, sign, pk) + if err != nil { + panic(err) + } + // An http server will use the tls.Config like: + _ = &http.Server{ + Addr: ":443", + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("Hello world")) + }), + TLSConfig: tlsConfig, + } + + // Get tls.Config for a client + ctxClient, cancelClient := context.WithCancel(context.Background()) + defer cancelClient() + tlsConfig, err = client.GetClientTLSConfig(ctxClient, sign, pk) + // An http.Client will need to create a transport first + _ = &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: tlsConfig, + // Options set in http.DefaultTransport + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + DualStack: true, + }).DialContext, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + }, + } + + // But we can just use client.Transport to get the default configuration + ctxTransport, cancelTransport := context.WithCancel(context.Background()) + defer cancelTransport() + tr, err = client.Transport(ctxTransport, sign, pk) + // And http.Client will use the transport like + _ = &http.Client{ + Transport: tr, + } + + // Get provisioners and provisioner keys. In this example we add two + // optional arguments with the initial cursor and a limit. + // + // A server or a client should not need this functionality, they are used to + // sign (private key) and verify (public key) tokens. The step cli can be + // used for this purpose. + provisioners, err := client.Provisioners(ca.WithProvisionerCursor(""), ca.WithProvisionerLimit(100)) + if err != nil { + panic(err) + } + printResponse("Provisioners", provisioners) + // Get encrypted key + key, err := client.ProvisionerKey("DmAtZt2EhmZr_iTJJ387fr4Md2NbzMXGdXQNW1UWPXk") + if err != nil { + panic(err) + } + printResponse("Provisioner Key", key) +}