forked from TrueCloudLab/certificates
Add new example and docs on the client SDK.
This commit is contained in:
parent
7eb8aeb1f1
commit
6617c93732
2 changed files with 287 additions and 4 deletions
|
@ -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
|
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
|
as a simple Server using TLS and a simple client doing TLS requests to the
|
||||||
|
|
152
examples/basic-client/client.go
Normal file
152
examples/basic-client/client.go
Normal file
|
@ -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 <token>\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)
|
||||||
|
}
|
Loading…
Reference in a new issue