forked from TrueCloudLab/certificates
commit
4518be9fd9
13 changed files with 888 additions and 12 deletions
114
ca/bootstrap.go
Normal file
114
ca/bootstrap.go
Normal file
|
@ -0,0 +1,114 @@
|
|||
package ca
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/cli/jose"
|
||||
"gopkg.in/square/go-jose.v2/jwt"
|
||||
)
|
||||
|
||||
type tokenClaims struct {
|
||||
SHA string `json:"sha"`
|
||||
jose.Claims
|
||||
}
|
||||
|
||||
// Bootstrap is a helper function that initializes a client with the
|
||||
// configuration in the bootstrap token.
|
||||
func Bootstrap(token string) (*Client, error) {
|
||||
tok, err := jwt.ParseSigned(token)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error parsing token")
|
||||
}
|
||||
var claims tokenClaims
|
||||
if err := tok.UnsafeClaimsWithoutVerification(&claims); err != nil {
|
||||
return nil, errors.Wrap(err, "error parsing token")
|
||||
}
|
||||
|
||||
// Validate bootstrap token
|
||||
switch {
|
||||
case len(claims.SHA) == 0:
|
||||
return nil, errors.New("invalid bootstrap token: sha claim is not present")
|
||||
case !strings.HasPrefix(strings.ToLower(claims.Audience[0]), "http"):
|
||||
return nil, errors.New("invalid bootstrap token: aud claim is not a url")
|
||||
}
|
||||
|
||||
return NewClient(claims.Audience[0], WithRootSHA256(claims.SHA))
|
||||
}
|
||||
|
||||
// BootstrapServer is a helper function that returns an http.Server configured
|
||||
// with the given address and handler, and prepared to use TLS connections. The
|
||||
// certificate will automatically rotate if necessary.
|
||||
//
|
||||
// Usage:
|
||||
// srv, err := ca.BootstrapServer(":443", token, handler)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// srv.ListenAndServeTLS("", "")
|
||||
func BootstrapServer(addr, token string, handler http.Handler) (*http.Server, error) {
|
||||
client, err := Bootstrap(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, pk, err := CreateSignRequest(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sign, err := client.Sign(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tlsConfig, err := client.GetServerTLSConfig(context.Background(), sign, pk)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &http.Server{
|
||||
Addr: addr,
|
||||
Handler: handler,
|
||||
TLSConfig: tlsConfig,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// BootstrapClient is a helper function that using the given bootstrap token
|
||||
// return an http.Client configured with a Transport prepared to do TLS
|
||||
// connections using the client certificate returned by the certificate
|
||||
// authority. The certificate will automatically rotate if necessary.
|
||||
//
|
||||
// Usage:
|
||||
// client, err := ca.BootstrapClient(token)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// resp, err := client.Get("https://internal.smallstep.com")
|
||||
func BootstrapClient(token string) (*http.Client, error) {
|
||||
client, err := Bootstrap(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, pk, err := CreateSignRequest(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sign, err := client.Sign(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
transport, err := client.Transport(context.Background(), sign, pk)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &http.Client{
|
||||
Transport: transport,
|
||||
}, nil
|
||||
}
|
219
ca/bootstrap_test.go
Normal file
219
ca/bootstrap_test.go
Normal file
|
@ -0,0 +1,219 @@
|
|||
package ca
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/smallstep/certificates/api"
|
||||
"github.com/smallstep/certificates/authority"
|
||||
|
||||
"github.com/smallstep/cli/crypto/randutil"
|
||||
stepJOSE "github.com/smallstep/cli/jose"
|
||||
jose "gopkg.in/square/go-jose.v2"
|
||||
"gopkg.in/square/go-jose.v2/jwt"
|
||||
)
|
||||
|
||||
func startCABootstrapServer() *httptest.Server {
|
||||
config, err := authority.LoadConfiguration("testdata/ca.json")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
srv := httptest.NewUnstartedServer(nil)
|
||||
config.Address = srv.Listener.Addr().String()
|
||||
ca, err := New(config)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
srv.Config.Handler = ca.srv.Handler
|
||||
srv.TLS = ca.srv.TLSConfig
|
||||
srv.StartTLS()
|
||||
// Force the use of GetCertificate on IPs
|
||||
srv.TLS.Certificates = nil
|
||||
return srv
|
||||
}
|
||||
|
||||
func generateBootstrapToken(ca, subject, sha string) string {
|
||||
now := time.Now()
|
||||
jwk, err := stepJOSE.ParseKey("testdata/secrets/ott_mariano_priv.jwk", stepJOSE.WithPassword([]byte("password")))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
opts := new(jose.SignerOptions).WithType("JWT").WithHeader("kid", jwk.KeyID)
|
||||
sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, opts)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
id, err := randutil.ASCII(64)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
cl := struct {
|
||||
SHA string `json:"sha"`
|
||||
jwt.Claims
|
||||
}{
|
||||
SHA: sha,
|
||||
Claims: jwt.Claims{
|
||||
ID: id,
|
||||
Subject: subject,
|
||||
Issuer: "mariano",
|
||||
NotBefore: jwt.NewNumericDate(now),
|
||||
Expiry: jwt.NewNumericDate(now.Add(time.Minute)),
|
||||
Audience: []string{ca + "/sign"},
|
||||
},
|
||||
}
|
||||
raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
func TestBootstrap(t *testing.T) {
|
||||
srv := startCABootstrapServer()
|
||||
defer srv.Close()
|
||||
token := generateBootstrapToken(srv.URL, "subject", "ef742f95dc0d8aa82d3cca4017af6dac3fce84290344159891952d18c53eefe7")
|
||||
client, err := NewClient(srv.URL+"/sign", WithRootFile("testdata/secrets/root_ca.crt"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
type args struct {
|
||||
token string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *Client
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", args{token}, client, false},
|
||||
{"token err", args{"badtoken"}, nil, true},
|
||||
{"bad claims", args{"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.foo.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"}, nil, true},
|
||||
{"bad sha", args{generateBootstrapToken(srv.URL, "subject", "")}, nil, true},
|
||||
{"bad aud", args{generateBootstrapToken("", "subject", "ef742f95dc0d8aa82d3cca4017af6dac3fce84290344159891952d18c53eefe7")}, nil, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Bootstrap(tt.args.token)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Bootstrap() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if tt.wantErr {
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Bootstrap() = %v, want %v", got, tt.want)
|
||||
}
|
||||
} else {
|
||||
if got == nil {
|
||||
t.Error("Bootstrap() = nil, want not nil")
|
||||
} else {
|
||||
if !reflect.DeepEqual(got.endpoint, tt.want.endpoint) {
|
||||
t.Errorf("Bootstrap() endpoint = %v, want %v", got.endpoint, tt.want.endpoint)
|
||||
}
|
||||
if !reflect.DeepEqual(got.certPool, tt.want.certPool) {
|
||||
t.Errorf("Bootstrap() certPool = %v, want %v", got.certPool, tt.want.certPool)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBootstrapServer(t *testing.T) {
|
||||
srv := startCABootstrapServer()
|
||||
defer srv.Close()
|
||||
token := func() string {
|
||||
return generateBootstrapToken(srv.URL, "subject", "ef742f95dc0d8aa82d3cca4017af6dac3fce84290344159891952d18c53eefe7")
|
||||
}
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("ok"))
|
||||
})
|
||||
type args struct {
|
||||
addr string
|
||||
token string
|
||||
handler http.Handler
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", args{":0", token(), handler}, false},
|
||||
{"fail", args{":0", "bad-token", handler}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := BootstrapServer(tt.args.addr, tt.args.token, tt.args.handler)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("BootstrapServer() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if tt.wantErr {
|
||||
if got != nil {
|
||||
t.Errorf("BootstrapServer() = %v, want nil", got)
|
||||
}
|
||||
} else {
|
||||
if !reflect.DeepEqual(got.Addr, tt.args.addr) {
|
||||
t.Errorf("BootstrapServer() Addr = %v, want %v", got.Addr, tt.args.addr)
|
||||
}
|
||||
if got.TLSConfig == nil || got.TLSConfig.ClientCAs == nil || got.TLSConfig.RootCAs == nil || got.TLSConfig.GetCertificate == nil || got.TLSConfig.GetClientCertificate == nil {
|
||||
t.Errorf("BootstrapServer() invalid TLSConfig = %#v", got.TLSConfig)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBootstrapClient(t *testing.T) {
|
||||
srv := startCABootstrapServer()
|
||||
defer srv.Close()
|
||||
token := func() string {
|
||||
return generateBootstrapToken(srv.URL, "subject", "ef742f95dc0d8aa82d3cca4017af6dac3fce84290344159891952d18c53eefe7")
|
||||
}
|
||||
type args struct {
|
||||
token string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", args{token()}, false},
|
||||
{"fail", args{"bad-token"}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := BootstrapClient(tt.args.token)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("BootstrapClient() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if tt.wantErr {
|
||||
if got != nil {
|
||||
t.Errorf("BootstrapClient() = %v, want nil", got)
|
||||
}
|
||||
} else {
|
||||
tlsConfig := got.Transport.(*http.Transport).TLSClientConfig
|
||||
if tlsConfig == nil || tlsConfig.ClientCAs != nil || tlsConfig.GetClientCertificate == nil || tlsConfig.RootCAs == nil || tlsConfig.GetCertificate != nil {
|
||||
t.Errorf("BootstrapClient() invalid Transport = %#v", tlsConfig)
|
||||
}
|
||||
resp, err := got.Post(srv.URL+"/renew", "application/json", http.NoBody)
|
||||
if err != nil {
|
||||
t.Errorf("BootstrapClient() failed renewing certificate")
|
||||
return
|
||||
}
|
||||
var renewal api.SignResponse
|
||||
if err := readJSON(resp.Body, &renewal); err != nil {
|
||||
t.Errorf("BootstrapClient() error reading response: %v", err)
|
||||
return
|
||||
}
|
||||
if renewal.CaPEM.Certificate == nil || renewal.ServerPEM.Certificate == nil {
|
||||
t.Errorf("BootstrapClient() invalid renewal response: %v", renewal)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
227
examples/README.md
Normal file
227
examples/README.md
Normal file
|
@ -0,0 +1,227 @@
|
|||
# Examples
|
||||
|
||||
## 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.GetServerTLSConfig(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
|
||||
server.
|
||||
|
||||
The examples directory already contains a sample pki configuration with the
|
||||
password `password` hardcoded, but you can create your own using `step ca init`.
|
||||
|
||||
First we will start the certificate authority:
|
||||
```
|
||||
certificates $ bin/step-ca examples/pki/config/ca.json
|
||||
2018/11/02 18:29:25 Serving HTTPS on :9000 ...
|
||||
```
|
||||
|
||||
We will start the server and we will type `password` when step asks for the
|
||||
provisioner password:
|
||||
```
|
||||
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)
|
||||
Please enter the password to decrypt the provisioner key:
|
||||
Listening on :8443 ...
|
||||
```
|
||||
|
||||
We try that using cURL with the system certificates it will return an error:
|
||||
```
|
||||
certificates $ curl https://localhost:8443
|
||||
curl: (60) SSL certificate problem: unable to get local issuer certificate
|
||||
More details here: https://curl.haxx.se/docs/sslcerts.html
|
||||
|
||||
curl performs SSL certificate verification by default, using a "bundle"
|
||||
of Certificate Authority (CA) public keys (CA certs). If the default
|
||||
bundle file isn't adequate, you can specify an alternate file
|
||||
using the --cacert option.
|
||||
If this HTTPS server uses a certificate signed by a CA represented in
|
||||
the bundle, the certificate verification probably failed due to a
|
||||
problem with the certificate (it might be expired, or the name might
|
||||
not match the domain name in the URL).
|
||||
If you'd like to turn off curl's verification of the certificate, use
|
||||
the -k (or --insecure) option.
|
||||
HTTPS-proxy has similar options --proxy-cacert and --proxy-insecure.
|
||||
```
|
||||
|
||||
But if we use the root certificate it will properly work:
|
||||
```
|
||||
certificates $ curl --cacert examples/pki/secrets/root_ca.crt https://localhost:8443
|
||||
Hello nobody at 2018-11-03 01:49:25.66912 +0000 UTC!!!
|
||||
```
|
||||
|
||||
Notice that in the response we see `nobody`, this is because the server didn't
|
||||
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 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)
|
||||
Please enter the password to decrypt the provisioner key:
|
||||
Server responded: Hello Mike at 2018-11-03 01:52:52.678215 +0000 UTC!!!
|
||||
Server responded: Hello Mike at 2018-11-03 01:52:53.681563 +0000 UTC!!!
|
||||
Server responded: Hello Mike at 2018-11-03 01:52:54.682787 +0000 UTC!!!
|
||||
...
|
||||
```
|
||||
|
||||
## Certificate rotation
|
||||
|
||||
We can use the bootstrap-server to demonstrate the certificate rotation. We've
|
||||
added second provisioner to to the ca with the name of `mike@smallstep.com`,
|
||||
this provisioner is configured with a default certificate duration of 2 minutes.
|
||||
If we run the server, and inspect the used certificate, we can verify how it
|
||||
rotates after approximately two thirds of the duration has passed.
|
||||
|
||||
```
|
||||
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: YYNxZ0rq0WsT2MlqLCWvgme3jszkmt99KjoGEJJwAKs (mike@smallstep.com)
|
||||
Please enter the password to decrypt the provisioner key:
|
||||
Listening on :8443 ...
|
||||
```
|
||||
|
||||
In this specific case, the the rotation will happen after 74-80 seconds have
|
||||
passed, the exact formula is 120-120/3-rand(120/20), where rand will return a
|
||||
number between 0 and 6.
|
||||
|
||||
We can use the following command to check the certificate expiration and to make
|
||||
sure the certificate changes after 74-80 seconds.
|
||||
|
||||
```
|
||||
certificates $ step certificate inspect --insecure https://localhost:8443
|
||||
```
|
158
examples/basic-client/client.go
Normal file
158
examples/basic-client/client.go
Normal file
|
@ -0,0 +1,158 @@
|
|||
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)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// 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)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// 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)
|
||||
}
|
39
examples/bootstrap-client/client.go
Normal file
39
examples/bootstrap-client/client.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/smallstep/certificates/ca"
|
||||
)
|
||||
|
||||
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]
|
||||
|
||||
client, err := ca.BootstrapClient(token)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for {
|
||||
resp, err := client.Get("https://localhost:8443")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Printf("Server responded: %s\n", b)
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
35
examples/bootstrap-server/server.go
Normal file
35
examples/bootstrap-server/server.go
Normal file
|
@ -0,0 +1,35 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/smallstep/certificates/ca"
|
||||
)
|
||||
|
||||
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]
|
||||
|
||||
srv, err := ca.BootstrapServer(":8443", token, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
name := "nobody"
|
||||
if r.TLS != nil && len(r.TLS.PeerCertificates) > 0 {
|
||||
name = r.TLS.PeerCertificates[0].Subject.CommonName
|
||||
}
|
||||
w.Write([]byte(fmt.Sprintf("Hello %s at %s!!!", name, time.Now().UTC())))
|
||||
}))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Println("Listening on :8443 ...")
|
||||
if err := srv.ListenAndServeTLS("", ""); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"caPath": "/path/to/intermediate-certificate",
|
||||
"caPrivateKeyPath": "/path/to/intermediate-private-key",
|
||||
"caPasscode": "very-secure-passcode",
|
||||
"listenAddress": "127.0.0.1:9000"
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
country: USA
|
||||
locality: San Francisco
|
||||
organization: smallstep
|
||||
common_name: internal.smallstep.com
|
||||
key_type: rsa
|
||||
rsa_bits: 4096
|
58
examples/pki/config/ca.json
Normal file
58
examples/pki/config/ca.json
Normal file
|
@ -0,0 +1,58 @@
|
|||
{
|
||||
"root": "examples/pki/secrets/root_ca.crt",
|
||||
"crt": "examples/pki/secrets/intermediate_ca.crt",
|
||||
"key": "examples/pki/secrets/intermediate_ca_key",
|
||||
"password": "password",
|
||||
"address": ":9000",
|
||||
"dnsNames": [
|
||||
"localhost"
|
||||
],
|
||||
"logger": {
|
||||
"format": "text"
|
||||
},
|
||||
"authority": {
|
||||
"provisioners": [
|
||||
{
|
||||
"name": "mariano@smallstep.com",
|
||||
"type": "jwk",
|
||||
"key": {
|
||||
"use": "sig",
|
||||
"kty": "EC",
|
||||
"kid": "DmAtZt2EhmZr_iTJJ387fr4Md2NbzMXGdXQNW1UWPXk",
|
||||
"crv": "P-256",
|
||||
"alg": "ES256",
|
||||
"x": "jXoO1j4CXxoTC32pNzkVC8l6k2LfP0k5ndhJZmcdVbk",
|
||||
"y": "c3JDL4GTFxJWHa8EaHdMh4QgwMh64P2_AGWrD0ADXcI"
|
||||
},
|
||||
"encryptedKey": "eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiOTFVWjdzRGw3RlNXcldfX1I1NUh3USJ9.FcWtrBDNgrkA33G9Ll9sXh1cPF-3jVXeYe1FLmSDc_Q2PmfLOPvJOA.0ZoN32ayaRWnufJb.WrkffMmDLWiq1-2kn-w7-kVBGW12gjNCBHNHB1hyEdED0rWH1YWpKd8FjoOACdJyLhSn4kAS3Lw5AH7fvO27A48zzvoxZU5EgSm5HG9IjkIH-LBJ-v79ShkpmPylchgjkFhxa5epD11OIK4rFmI7s-0BCjmJokLR_DZBhDMw2khGnsr_MEOfAz9UnqXaQ4MIy8eT52xUpx68gpWFlz2YP3EqiYyNEv0PpjMtyP5lO2i8-p8BqvuJdus9H3fO5Dg-1KVto1wuqh4BQ2JKTauv60QAnM_4sdxRHku3F_nV64SCrZfDvnN2ve21raFROtyXaqHZhN6lyoPxDncy8v4.biaOblEe0N-gMpJyFZ-3-A"
|
||||
},
|
||||
{
|
||||
"name": "mike@smallstep.com",
|
||||
"type": "jwk",
|
||||
"key": {
|
||||
"use": "sig",
|
||||
"kty": "EC",
|
||||
"kid": "YYNxZ0rq0WsT2MlqLCWvgme3jszkmt99KjoGEJJwAKs",
|
||||
"crv": "P-256",
|
||||
"alg": "ES256",
|
||||
"x": "LsI8nHBflc-mrCbRqhl8d3hSl5sYuSM1AbXBmRfznyg",
|
||||
"y": "F99LoOvi7z-ZkumsgoHIhodP8q9brXe4bhF3szK-c_w"
|
||||
},
|
||||
"encryptedKey": "eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiVERQS2dzcEItTUR4ZDJxTGo0VlpwdyJ9.2_j0cZgTm2eFkZ-hrtr1hBIvLxN0w3TZhbX0Jrrq7vBMaywhgFcGTA.mCasZCbZJ-JT7vjA.bW052WDKSf_ueEXq1dyxLq0n3qXWRO-LXr7OzBLdUKWKSBGQrzqS5KJWqdUCPoMIHTqpwYvm-iD6uFlcxKBYxnsAG_hoq_V3icvvwNQQSd_q7Thxr2_KtPIDJWNuX1t5qXp11hkgb-8d5HO93CmN7xNDG89pzSUepT6RYXOZ483mP5fre9qzkfnrjx3oPROCnf3SnIVUvqk7fwfXuniNsg3NrNqncHYUQNReiq3e9I1R60w0ZQTvIReY7-zfiq7iPgVqmu5I7XGgFK4iBv0L7UOEora65b4hRWeLxg5t7OCfUqrS9yxAk8FdjFb9sEfjopWViPRepB0dYPH8dVI.fb6-7XWqp0j6CR9Li0NI-Q",
|
||||
"claims": {
|
||||
"minTLSCertDuration": "60s",
|
||||
"defaultTLSCertDuration": "120s"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"tls": {
|
||||
"cipherSuites": [
|
||||
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305",
|
||||
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"
|
||||
],
|
||||
"minVersion": 1.2,
|
||||
"maxVersion": 1.2,
|
||||
"renegotiation": false
|
||||
}
|
||||
}
|
12
examples/pki/secrets/intermediate_ca.crt
Normal file
12
examples/pki/secrets/intermediate_ca.crt
Normal file
|
@ -0,0 +1,12 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIBxjCCAWugAwIBAgIQAYoOWhdChUmmKzlc0DWcWDAKBggqhkjOPQQDAjAcMRow
|
||||
GAYDVQQDExFTbWFsbHN0ZXAgUm9vdCBDQTAeFw0xODExMDIyMzU0MTNaFw0yODEw
|
||||
MzAyMzU0MTNaMCQxIjAgBgNVBAMTGVNtYWxsc3RlcCBJbnRlcm1lZGlhdGUgQ0Ew
|
||||
WTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASxvIWme8/yDAxkR63KgSYkpN7mHKBH
|
||||
k5c8S+uzba4xWbaxZtEZ9NNhEIAgYFZ9/3ThrzLOsuGwRCvPTaD5iycQo4GGMIGD
|
||||
MA4GA1UdDwEB/wQEAwIBpjAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIw
|
||||
EgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU8dKIy5ZLH2h3ihWgqjcpoo5e
|
||||
q3YwHwYDVR0jBBgwFoAU0IpOvAyBnn9UhDqOQzXnfEU3aYMwCgYIKoZIzj0EAwID
|
||||
SQAwRgIhANXlcktuaEvORhgRvzQ6vVNgvpqCEXW3CcCHjUl1xSdaAiEAmakkpfFq
|
||||
VsT5PqPnTRgOWlFESRhQ9btl6nQ+2Lt/S5A=
|
||||
-----END CERTIFICATE-----
|
8
examples/pki/secrets/intermediate_ca_key
Normal file
8
examples/pki/secrets/intermediate_ca_key
Normal file
|
@ -0,0 +1,8 @@
|
|||
-----BEGIN EC PRIVATE KEY-----
|
||||
Proc-Type: 4,ENCRYPTED
|
||||
DEK-Info: AES-256-CBC,4c7758e66df1884f6560839de64d4dd3
|
||||
|
||||
S8Ha8uA+bA3IGPurYODwd9VaJZ6FHI2tlznHXCOxT1MlGqyEAc4aWS11QBUz0Ucp
|
||||
excwlqM8kfh5BcN5a+vvInHnv74ZiNPdpt/apzz2LIx52pApzASiKVXRsAUmR4Pv
|
||||
3MsO1/cVHkilpee1uC+axL32d5YmyP0URpSNJK9BhZo=
|
||||
-----END EC PRIVATE KEY-----
|
10
examples/pki/secrets/root_ca.crt
Normal file
10
examples/pki/secrets/root_ca.crt
Normal file
|
@ -0,0 +1,10 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIBfDCCASGgAwIBAgIQY0CXerxuM+EhTbpVxxLRKjAKBggqhkjOPQQDAjAcMRow
|
||||
GAYDVQQDExFTbWFsbHN0ZXAgUm9vdCBDQTAeFw0xODExMDIyMzU0MTNaFw0yODEw
|
||||
MzAyMzU0MTNaMBwxGjAYBgNVBAMTEVNtYWxsc3RlcCBSb290IENBMFkwEwYHKoZI
|
||||
zj0CAQYIKoZIzj0DAQcDQgAEEGa7ZeL4WVIfPFDS7glJkIVsITVQgjfyz+AhcYaS
|
||||
rkJZlWOGZ60br9uE/wEfUcX1zavrX1Wz+bSJzTvT0AVBNqNFMEMwDgYDVR0PAQH/
|
||||
BAQDAgGmMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFNCKTrwMgZ5/VIQ6
|
||||
jkM153xFN2mDMAoGCCqGSM49BAMCA0kAMEYCIQCRA4EdlTTMhs2Zd1cT75ZgxeGa
|
||||
mjVPl1vqBxLkHqEO+QIhAPKVm7E452ZBe2o5rQRxGwa94MI+CyuEIH9md3nTgWWX
|
||||
-----END CERTIFICATE-----
|
8
examples/pki/secrets/root_ca_key
Normal file
8
examples/pki/secrets/root_ca_key
Normal file
|
@ -0,0 +1,8 @@
|
|||
-----BEGIN EC PRIVATE KEY-----
|
||||
Proc-Type: 4,ENCRYPTED
|
||||
DEK-Info: AES-256-CBC,98fdc560ba714aebb9fd4b714395d8ce
|
||||
|
||||
2bFn8yRb8lMvDR6oh22PocfhXdaoVNt4QwHCJNy0K0fG8CMokwDfEec//LseP6rA
|
||||
7/EV11+ZgoN9xyTNe1kB6zFv7/kzCpRm23sqtyio+8xXWnLZNYKBRYYEeJWBUqqd
|
||||
GAfazg4ZFzoIH5TEPWCEAp7M9CVvtiw1SeA/zjewp2k=
|
||||
-----END EC PRIVATE KEY-----
|
Loading…
Reference in a new issue