diff --git a/ca/client.go b/ca/client.go index a7cd0a7a..6c043ca7 100644 --- a/ca/client.go +++ b/ca/client.go @@ -27,6 +27,7 @@ import ( "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/cli/config" "github.com/smallstep/cli/crypto/x509util" + "golang.org/x/net/http2" "gopkg.in/square/go-jose.v2/jwt" ) @@ -38,9 +39,13 @@ type clientOptions struct { rootSHA256 string rootFilename string rootBundle []byte + certificate tls.Certificate } func (o *clientOptions) apply(opts []ClientOption) (err error) { + if err = o.applyDefaultIdentity(); err != nil { + return + } for _, fn := range opts { if err = fn(o); err != nil { return @@ -49,6 +54,32 @@ func (o *clientOptions) apply(opts []ClientOption) (err error) { return } +// applyDefaultIdentity sets the options for the default identity if the +// identity file is present. The identity is enabled by default. +func (o *clientOptions) applyDefaultIdentity() error { + b, err := ioutil.ReadFile(IdentityFile) + if err != nil { + return nil + } + var identity Identity + if err := json.Unmarshal(b, &identity); err != nil { + return errors.Wrapf(err, "error unmarshaling %s", IdentityFile) + } + if err := identity.Validate(); err != nil { + return err + } + opts, err := identity.Options() + if err != nil { + return err + } + for _, fn := range opts { + if err := fn(o); err != nil { + return err + } + } + return nil +} + // checkTransport checks if other ways to set up a transport have been provided. // If they have it returns an error. func (o *clientOptions) checkTransport() error { @@ -85,10 +116,28 @@ func (o *clientOptions) getTransport(endpoint string) (tr http.RoundTripper, err if tr, err = getTransportFromFile(rootFile); err != nil { return nil, err } - return tr, nil } - return nil, errors.New("a transport, a root cert, or a root sha256 must be used") + if tr == nil { + return nil, errors.New("a transport, a root cert, or a root sha256 must be used") + } } + + // Add client certificate if available + if o.certificate.Certificate != nil { + switch tr := tr.(type) { + case *http.Transport: + if len(tr.TLSClientConfig.Certificates) == 0 && tr.TLSClientConfig.GetClientCertificate == nil { + tr.TLSClientConfig.Certificates = []tls.Certificate{o.certificate} + } + case *http2.Transport: + if len(tr.TLSClientConfig.Certificates) == 0 && tr.TLSClientConfig.GetClientCertificate == nil { + tr.TLSClientConfig.Certificates = []tls.Certificate{o.certificate} + } + default: + return nil, errors.Errorf("unsupported transport type %T", tr) + } + } + return tr, nil } @@ -141,6 +190,15 @@ func WithCABundle(bundle []byte) ClientOption { } } +// WithCertificate will set the given certificate as the TLS client certificate +// in the client. +func WithCertificate(crt tls.Certificate) ClientOption { + return func(o *clientOptions) error { + o.certificate = crt + return nil + } +} + func getTransportFromFile(filename string) (http.RoundTripper, error) { data, err := ioutil.ReadFile(filename) if err != nil { diff --git a/ca/identity.go b/ca/identity.go new file mode 100644 index 00000000..15f8358c --- /dev/null +++ b/ca/identity.go @@ -0,0 +1,69 @@ +package ca + +import ( + "crypto/tls" + "path/filepath" + "strings" + + "github.com/pkg/errors" + "github.com/smallstep/cli/config" +) + +// IdentityType represents the different types of identity files. +type IdentityType string + +// MutualTLS represents the identity using mTLS +const MutualTLS IdentityType = "mTLS" + +// IdentityFile contains the location of the identity file. +var IdentityFile = filepath.Join(config.StepPath(), "config", "identity.json") + +// Identity represents the identity file that can be used to authenticate with +// the CA. +type Identity struct { + Type string `json:"type"` + Certificate string `json:"crt"` + Key string `json:"key"` +} + +// Kind returns the type for the given identity. +func (i *Identity) Kind() IdentityType { + switch strings.ToLower(i.Type) { + case "mtls": + return MutualTLS + default: + return IdentityType(i.Type) + } +} + +// Validate validates the identity object. +func (i *Identity) Validate() error { + switch i.Kind() { + case MutualTLS: + if i.Certificate == "" { + return errors.New("identity.crt cannot be empty") + } + if i.Key == "" { + return errors.New("identity.key cannot be empty") + } + return nil + case "": + return errors.New("identity.type cannot be empty") + default: + return errors.Errorf("unsupported identity type %s", i.Type) + } +} + +// Options returns the ClientOptions used for the given identity. +func (i *Identity) Options() ([]ClientOption, error) { + switch i.Kind() { + case MutualTLS: + crt, err := tls.LoadX509KeyPair(i.Certificate, i.Key) + if err != nil { + return nil, errors.Wrap(err, "error creating identity certificate") + } + return []ClientOption{WithCertificate(crt)}, nil + default: + return nil, errors.Errorf("unsupported identity type %s", i.Type) + } +}