forked from TrueCloudLab/certificates
392 lines
13 KiB
Go
392 lines
13 KiB
Go
package ca
|
|
|
|
import (
|
|
"context"
|
|
"crypto"
|
|
"crypto/ecdsa"
|
|
"crypto/ed25519"
|
|
"crypto/rsa"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"runtime"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/smallstep/certificates/api"
|
|
"github.com/smallstep/certificates/ca/identity"
|
|
)
|
|
|
|
// mTLSDialContext will hold the dial context function to use in
|
|
// getDefaultTransport.
|
|
var mTLSDialContext func() func(ctx context.Context, network, address string) (net.Conn, error)
|
|
|
|
// localAddr is the local address to use when dialing an address. This address
|
|
// is defined by the environment variable STEP_CLIENT_ADDR.
|
|
var localAddr net.Addr
|
|
|
|
func init() {
|
|
// STEP_TLS_TUNNEL is an environment variable that can be set to do an TLS
|
|
// over (m)TLS tunnel to step-ca using identity-like credentials. The value
|
|
// is a path to a json file with the tunnel host, certificate, key and root
|
|
// used to create the (m)TLS tunnel.
|
|
//
|
|
// The configuration should look like:
|
|
// {
|
|
// "type": "tTLS",
|
|
// "host": "tunnel.example.com:443"
|
|
// "crt": "/path/to/tunnel.crt",
|
|
// "key": "/path/to/tunnel.key",
|
|
// "root": "/path/to/tunnel-root.crt"
|
|
// }
|
|
//
|
|
// This feature is EXPERIMENTAL and might change at any time.
|
|
if path := os.Getenv("STEP_TLS_TUNNEL"); path != "" {
|
|
id, err := identity.LoadIdentity(path)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
if err := id.Validate(); err != nil {
|
|
panic(err)
|
|
}
|
|
host, port, err := net.SplitHostPort(id.Host)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
pool, err := id.GetCertPool()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
mTLSDialContext = func() func(ctx context.Context, network, address string) (net.Conn, error) {
|
|
d := &tls.Dialer{
|
|
NetDialer: getDefaultDialer(),
|
|
Config: &tls.Config{
|
|
MinVersion: tls.VersionTLS12,
|
|
RootCAs: pool,
|
|
GetClientCertificate: id.GetClientCertificateFunc(),
|
|
},
|
|
}
|
|
return func(ctx context.Context, network, address string) (net.Conn, error) {
|
|
return d.DialContext(ctx, "tcp", net.JoinHostPort(host, port))
|
|
}
|
|
}
|
|
}
|
|
|
|
// STEP_CLIENT_ADDR is an environment variable that can be set to define the
|
|
// local address to use when dialing an address. This can be useful when
|
|
// step is run behind a CIDR-based ACL.
|
|
//
|
|
// STEP_CLIENT_ADDR can be set to an IP ("127.0.0.1", "[::1]"), a hostname
|
|
// ("localhost"), or a host:port ("[::1]:0"). If the port is set to
|
|
// something other than ":0" and the dialer is created multiple times it
|
|
// will fail with an "address already in use" error.
|
|
//
|
|
// See https://github.com/smallstep/cli/issues/730
|
|
if v := os.Getenv("STEP_CLIENT_ADDR"); v != "" {
|
|
_, _, err := net.SplitHostPort(v)
|
|
if err != nil {
|
|
// assuming that the error is a missing port, if it's not it will
|
|
// panic below.
|
|
v += ":0"
|
|
}
|
|
localAddr, err = net.ResolveTCPAddr("tcp", v)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// GetClientTLSConfig returns a tls.Config for client use configured with the
|
|
// sign certificate, and a new certificate pool with the sign root certificate.
|
|
// The client certificate will automatically rotate before expiring.
|
|
func (c *Client) GetClientTLSConfig(ctx context.Context, sign *api.SignResponse, pk crypto.PrivateKey, options ...TLSOption) (*tls.Config, error) {
|
|
tlsConfig, _, err := c.getClientTLSConfig(ctx, sign, pk, options)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tlsConfig, nil
|
|
}
|
|
|
|
func (c *Client) getClientTLSConfig(ctx context.Context, sign *api.SignResponse, pk crypto.PrivateKey, options []TLSOption) (*tls.Config, *http.Transport, error) {
|
|
cert, err := TLSCertificate(sign, pk)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
renewer, err := NewTLSRenewer(cert, nil)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
tlsConfig := getDefaultTLSConfig(sign)
|
|
// Note that with GetClientCertificate tlsConfig.Certificates is not used.
|
|
// Without tlsConfig.Certificates there's not need to use tlsConfig.BuildNameToCertificate()
|
|
tlsConfig.GetClientCertificate = renewer.GetClientCertificate
|
|
|
|
// Apply options and initialize mutable tls.Config
|
|
tlsCtx := newTLSOptionCtx(c, tlsConfig, sign)
|
|
if err := tlsCtx.apply(options); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
tr := getDefaultTransport(tlsConfig)
|
|
//nolint:staticcheck // Use mutable tls.Config on renew
|
|
tr.DialTLS = c.buildDialTLS(tlsCtx)
|
|
// tr.DialTLSContext = c.buildDialTLSContext(tlsCtx)
|
|
renewer.RenewCertificate = getRenewFunc(tlsCtx, c, tr, pk) //nolint:contextcheck // deeply nested context
|
|
|
|
// Update client transport
|
|
c.SetTransport(tr)
|
|
|
|
// Start renewer
|
|
renewer.RunContext(ctx)
|
|
return tlsConfig, tr, nil
|
|
}
|
|
|
|
// GetServerTLSConfig returns a tls.Config for server use configured with the
|
|
// sign certificate, and a new certificate pool with the sign root certificate.
|
|
// The returned tls.Config will only verify the client certificate if provided.
|
|
// The server certificate will automatically rotate before expiring.
|
|
func (c *Client) GetServerTLSConfig(ctx context.Context, sign *api.SignResponse, pk crypto.PrivateKey, options ...TLSOption) (*tls.Config, error) {
|
|
cert, err := TLSCertificate(sign, pk)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
renewer, err := NewTLSRenewer(cert, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tlsConfig := getDefaultTLSConfig(sign)
|
|
// Note that GetCertificate will only be called if the client supplies SNI
|
|
// information or if tlsConfig.Certificates is empty.
|
|
// Without tlsConfig.Certificates there's not need to use tlsConfig.BuildNameToCertificate()
|
|
tlsConfig.GetCertificate = renewer.GetCertificate
|
|
tlsConfig.GetClientCertificate = renewer.GetClientCertificate
|
|
tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
|
|
|
|
// Apply options and initialize mutable tls.Config
|
|
tlsCtx := newTLSOptionCtx(c, tlsConfig, sign)
|
|
if err := tlsCtx.apply(options); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// GetConfigForClient allows seamless root and federated roots rotation.
|
|
// If the return of the callback is not-nil, it will use the returned
|
|
// tls.Config instead of the default one.
|
|
tlsConfig.GetConfigForClient = c.buildGetConfigForClient(tlsCtx)
|
|
|
|
// Update renew function with transport
|
|
tr := getDefaultTransport(tlsConfig)
|
|
//nolint:staticcheck // Use mutable tls.Config on renew
|
|
tr.DialTLS = c.buildDialTLS(tlsCtx)
|
|
// tr.DialTLSContext = c.buildDialTLSContext(tlsCtx)
|
|
renewer.RenewCertificate = getRenewFunc(tlsCtx, c, tr, pk) //nolint:contextcheck // deeply nested context
|
|
|
|
// Update client transport
|
|
c.SetTransport(tr)
|
|
|
|
// Start renewer
|
|
renewer.RunContext(ctx)
|
|
return tlsConfig, nil
|
|
}
|
|
|
|
// Transport returns an http.Transport configured to use the client certificate from the sign response.
|
|
func (c *Client) Transport(ctx context.Context, sign *api.SignResponse, pk crypto.PrivateKey, options ...TLSOption) (*http.Transport, error) {
|
|
_, tr, err := c.getClientTLSConfig(ctx, sign, pk, options)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tr, nil
|
|
}
|
|
|
|
// buildGetConfigForClient returns an implementation of GetConfigForClient
|
|
// callback in tls.Config.
|
|
//
|
|
// If the implementation returns a nil tls.Config, the original Config will be
|
|
// used, but if it's non-nil, the returned Config will be used to handle this
|
|
// connection.
|
|
func (c *Client) buildGetConfigForClient(ctx *TLSOptionCtx) func(*tls.ClientHelloInfo) (*tls.Config, error) {
|
|
return func(*tls.ClientHelloInfo) (*tls.Config, error) {
|
|
return ctx.mutableConfig.TLSConfig(), nil
|
|
}
|
|
}
|
|
|
|
// buildDialTLS returns an implementation of DialTLS callback in http.Transport.
|
|
func (c *Client) buildDialTLS(ctx *TLSOptionCtx) func(network, addr string) (net.Conn, error) {
|
|
return func(network, addr string) (net.Conn, error) {
|
|
return tls.DialWithDialer(getDefaultDialer(), network, addr, ctx.mutableConfig.TLSConfig())
|
|
}
|
|
}
|
|
|
|
//nolint:unused // buildDialTLSContext returns an implementation of DialTLSContext callback in http.Transport.
|
|
func (c *Client) buildDialTLSContext(tlsCtx *TLSOptionCtx) func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
return func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
d := getDefaultDialer()
|
|
// TLS dialers do not support context, but we can use the context
|
|
// deadline if it is set.
|
|
if t, ok := ctx.Deadline(); ok {
|
|
d.Deadline = t
|
|
}
|
|
return tls.DialWithDialer(d, network, addr, tlsCtx.mutableConfig.TLSConfig())
|
|
}
|
|
}
|
|
|
|
// Certificate returns the server or client certificate from the sign response.
|
|
func Certificate(sign *api.SignResponse) (*x509.Certificate, error) {
|
|
if sign.ServerPEM.Certificate == nil {
|
|
return nil, errors.New("ca: certificate does not exist")
|
|
}
|
|
return sign.ServerPEM.Certificate, nil
|
|
}
|
|
|
|
// IntermediateCertificate returns the CA intermediate certificate from the sign
|
|
// response.
|
|
func IntermediateCertificate(sign *api.SignResponse) (*x509.Certificate, error) {
|
|
if sign.CaPEM.Certificate == nil {
|
|
return nil, errors.New("ca: certificate does not exist")
|
|
}
|
|
return sign.CaPEM.Certificate, nil
|
|
}
|
|
|
|
// RootCertificate returns the root certificate from the sign response.
|
|
func RootCertificate(sign *api.SignResponse) (*x509.Certificate, error) {
|
|
if sign == nil || sign.TLS == nil || len(sign.TLS.VerifiedChains) == 0 {
|
|
return nil, errors.New("ca: certificate does not exist")
|
|
}
|
|
lastChain := sign.TLS.VerifiedChains[len(sign.TLS.VerifiedChains)-1]
|
|
if len(lastChain) == 0 {
|
|
return nil, errors.New("ca: certificate does not exist")
|
|
}
|
|
return lastChain[len(lastChain)-1], nil
|
|
}
|
|
|
|
// TLSCertificate creates a new TLS certificate from the sign response and the
|
|
// private key used.
|
|
func TLSCertificate(sign *api.SignResponse, pk crypto.PrivateKey) (*tls.Certificate, error) {
|
|
certPEM, err := getPEM(sign.ServerPEM)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
caPEM, err := getPEM(sign.CaPEM)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
keyPEM, err := getPEM(pk)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
//nolint:gocritic // using a new variable for clarity
|
|
chain := append(certPEM, caPEM...)
|
|
cert, err := tls.X509KeyPair(chain, keyPEM)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "error creating tls certificate")
|
|
}
|
|
leaf, err := x509.ParseCertificate(cert.Certificate[0])
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "error parsing tls certificate")
|
|
}
|
|
cert.Leaf = leaf
|
|
return &cert, nil
|
|
}
|
|
|
|
func getDefaultTLSConfig(sign *api.SignResponse) *tls.Config {
|
|
if sign.TLSOptions != nil {
|
|
return sign.TLSOptions.TLSConfig()
|
|
}
|
|
return &tls.Config{
|
|
MinVersion: tls.VersionTLS12,
|
|
}
|
|
}
|
|
|
|
// getDefaultDialer returns a new dialer with the default configuration.
|
|
func getDefaultDialer() *net.Dialer {
|
|
// With the KeepAlive parameter set to 0, it will be use Golang's default.
|
|
return &net.Dialer{
|
|
Timeout: 30 * time.Second,
|
|
LocalAddr: localAddr,
|
|
}
|
|
}
|
|
|
|
// getDefaultTransport returns an http.Transport with the same parameters than
|
|
// http.DefaultTransport, but adds the given tls.Config and configures the
|
|
// transport for HTTP/2.
|
|
func getDefaultTransport(tlsConfig *tls.Config) *http.Transport {
|
|
var dialContext func(ctx context.Context, network string, addr string) (net.Conn, error)
|
|
switch {
|
|
case runtime.GOOS == "js" && runtime.GOARCH == "wasm":
|
|
// when running in js/wasm and using the default dialer context all requests
|
|
// performed by the CA client resulted in a "protocol not supported" error.
|
|
// By setting the dial context to nil requests will be handled by the browser
|
|
// fetch API instead. Currently this will always set the dial context to nil,
|
|
// but we could implement some additional logic similar to what's found in
|
|
// https://github.com/golang/go/pull/46923/files to support a different dial
|
|
// context if it is available, required and expected to work.
|
|
dialContext = nil
|
|
case mTLSDialContext == nil:
|
|
d := getDefaultDialer()
|
|
dialContext = d.DialContext
|
|
default:
|
|
dialContext = mTLSDialContext()
|
|
}
|
|
return &http.Transport{
|
|
Proxy: http.ProxyFromEnvironment,
|
|
DialContext: dialContext,
|
|
ForceAttemptHTTP2: true,
|
|
MaxIdleConns: 100,
|
|
IdleConnTimeout: 90 * time.Second,
|
|
TLSHandshakeTimeout: 10 * time.Second,
|
|
ExpectContinueTimeout: 1 * time.Second,
|
|
TLSClientConfig: tlsConfig,
|
|
}
|
|
}
|
|
|
|
func getPEM(i interface{}) ([]byte, error) {
|
|
block := new(pem.Block)
|
|
switch i := i.(type) {
|
|
case api.Certificate:
|
|
block.Type = "CERTIFICATE"
|
|
block.Bytes = i.Raw
|
|
case *x509.Certificate:
|
|
block.Type = "CERTIFICATE"
|
|
block.Bytes = i.Raw
|
|
case *rsa.PrivateKey:
|
|
block.Type = "RSA PRIVATE KEY"
|
|
block.Bytes = x509.MarshalPKCS1PrivateKey(i)
|
|
case *ecdsa.PrivateKey:
|
|
var err error
|
|
block.Type = "EC PRIVATE KEY"
|
|
block.Bytes, err = x509.MarshalECPrivateKey(i)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "error marshaling private key")
|
|
}
|
|
case ed25519.PrivateKey:
|
|
var err error
|
|
block.Type = "PRIVATE KEY"
|
|
block.Bytes, err = x509.MarshalPKCS8PrivateKey(i)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "error marshaling private key")
|
|
}
|
|
default:
|
|
return nil, errors.Errorf("unsupported key type %T", i)
|
|
}
|
|
return pem.EncodeToMemory(block), nil
|
|
}
|
|
|
|
func getRenewFunc(ctx *TLSOptionCtx, client *Client, tr *http.Transport, pk crypto.PrivateKey) RenewFunc {
|
|
return func() (*tls.Certificate, error) {
|
|
// Get updated list of roots
|
|
if err := ctx.applyRenew(); err != nil {
|
|
return nil, err
|
|
}
|
|
// Get new certificate
|
|
sign, err := client.Renew(tr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return TLSCertificate(sign, pk)
|
|
}
|
|
}
|