package ca

import (
	"bytes"
	"context"
	"crypto"
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/sha256"
	"crypto/tls"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/hex"
	"encoding/json"
	"encoding/pem"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/pkg/errors"
	"golang.org/x/net/http2"
	"google.golang.org/protobuf/encoding/protojson"
	"google.golang.org/protobuf/proto"

	"github.com/smallstep/cli-utils/step"
	"go.step.sm/crypto/jose"
	"go.step.sm/crypto/keyutil"
	"go.step.sm/crypto/pemutil"
	"go.step.sm/crypto/randutil"
	"go.step.sm/crypto/x509util"

	"github.com/smallstep/certificates/api"
	"github.com/smallstep/certificates/authority"
	"github.com/smallstep/certificates/authority/provisioner"
	"github.com/smallstep/certificates/ca/client"
	"github.com/smallstep/certificates/ca/identity"
	"github.com/smallstep/certificates/errs"
)

// DisableIdentity is a global variable to disable the identity.
var DisableIdentity = false

// UserAgent will set the User-Agent header in the client requests.
var UserAgent = "step-http-client/1.0"

type uaClient struct {
	Client *http.Client
}

func newClient(transport http.RoundTripper, timeout time.Duration) *uaClient {
	return &uaClient{
		Client: &http.Client{
			Transport: transport,
			Timeout:   timeout,
		},
	}
}

//nolint:gosec // used in bootstrap protocol
func newInsecureClient() *uaClient {
	return &uaClient{
		Client: &http.Client{
			Transport: getDefaultTransport(&tls.Config{InsecureSkipVerify: true}),
		},
	}
}

func (c *uaClient) GetTransport() http.RoundTripper {
	return c.Client.Transport
}

func (c *uaClient) SetTransport(tr http.RoundTripper) {
	c.Client.Transport = tr
}

func (c *uaClient) CloseIdleConnections() {
	c.Client.CloseIdleConnections()
}

func (c *uaClient) Get(u string) (*http.Response, error) {
	return c.GetWithContext(context.Background(), u)
}

func (c *uaClient) GetWithContext(ctx context.Context, u string) (*http.Response, error) {
	req, err := http.NewRequestWithContext(ctx, "GET", u, http.NoBody)
	if err != nil {
		return nil, errors.Wrapf(err, "create GET %s request failed", u)
	}
	return c.Do(req)
}

func (c *uaClient) Post(u, contentType string, body io.Reader) (*http.Response, error) {
	return c.PostWithContext(context.Background(), u, contentType, body)
}

func (c *uaClient) PostWithContext(ctx context.Context, u, contentType string, body io.Reader) (*http.Response, error) {
	req, err := http.NewRequestWithContext(ctx, "POST", u, body)
	if err != nil {
		return nil, errors.Wrapf(err, "create POST %s request failed", u)
	}
	req.Header.Set("Content-Type", contentType)
	return c.Do(req)
}

// requestIDHeader is the header name used for propagating request IDs from
// the CA client to the CA and back again.
const requestIDHeader = "X-Request-Id"

// newRequestID generates a new random UUIDv4 request ID. If it fails,
// the request ID will be the empty string.
func newRequestID() string {
	requestID, err := randutil.UUIDv4()
	if err != nil {
		return ""
	}

	return requestID
}

// enforceRequestID checks if the X-Request-Id HTTP header is filled. If it's
// empty, the context is searched for a request ID. If that's also empty, a new
// request ID is generated.
func enforceRequestID(r *http.Request) {
	if requestID := r.Header.Get(requestIDHeader); requestID == "" {
		if reqID, ok := client.RequestIDFromContext(r.Context()); ok {
			// TODO(hs): ensure the request ID from the context is fresh, and thus hasn't been
			// used before by the client (unless it's a retry for the same request)?
			requestID = reqID
		} else {
			requestID = newRequestID()
		}
		r.Header.Set(requestIDHeader, requestID)
	}
}

func (c *uaClient) Do(req *http.Request) (*http.Response, error) {
	req.Header.Set("User-Agent", UserAgent)
	enforceRequestID(req)
	return c.Client.Do(req) //nolint:gosec // request to user-configured CA server
}

// RetryFunc defines the method used to retry a request. If it returns true, the
// request will be retried once.
type RetryFunc func(code int) bool

// ClientOption is the type of options passed to the Client constructor.
type ClientOption func(o *clientOptions) error

// TransportDecorator is the type used to support customization of the HTTP
// transport.
type TransportDecorator func(http.RoundTripper) http.RoundTripper

type clientOptions struct {
	transport            http.RoundTripper
	transportDecorator   TransportDecorator
	timeout              time.Duration
	rootSHA256           string
	rootFilename         string
	rootBundle           []byte
	certificate          tls.Certificate
	getClientCertificate func(*tls.CertificateRequestInfo) (*tls.Certificate, error)
	retryFunc            RetryFunc
	x5cJWK               *jose.JSONWebKey
	x5cCertFile          string
	x5cCertStrs          []string
	x5cCert              *x509.Certificate
	x5cSubject           string
}

func (o *clientOptions) apply(opts []ClientOption) (err error) {
	o.applyDefaultIdentity()
	for _, fn := range opts {
		if err = fn(o); err != nil {
			return
		}
	}
	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() {
	if DisableIdentity {
		return
	}

	// Do not load an identity if something fails
	i, err := identity.LoadDefaultIdentity()
	if err != nil {
		return
	}
	if err := i.Validate(); err != nil {
		return
	}
	crt, err := i.TLSCertificate()
	if err != nil {
		return
	}
	o.certificate = crt
	o.getClientCertificate = i.GetClientCertificateFunc()
}

// 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 {
	if o.transport != nil || o.rootFilename != "" || o.rootSHA256 != "" || o.rootBundle != nil {
		return errors.New("multiple transport methods have been configured")
	}
	return nil
}

// getTransport returns the transport configured in the clientOptions.
func (o *clientOptions) getTransport(endpoint string) (tr http.RoundTripper, err error) {
	if o.transport != nil {
		tr = o.transport
	}
	if o.rootFilename != "" {
		if tr, err = getTransportFromFile(o.rootFilename); err != nil {
			return nil, err
		}
	}
	if o.rootSHA256 != "" {
		if tr, err = getTransportFromSHA256(endpoint, o.rootSHA256); err != nil {
			return nil, err
		}
	}
	if o.rootBundle != nil {
		if tr, err = getTransportFromCABundle(o.rootBundle); err != nil {
			return nil, err
		}
	}
	// As the last option attempt to load the default root ca
	if tr == nil {
		rootFile := getRootCAPath()
		if _, err := os.Stat(rootFile); err == nil {
			if tr, err = getTransportFromFile(rootFile); err != nil {
				return nil, err
			}
		}
		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 tr.TLSClientConfig == nil {
				tr.TLSClientConfig = &tls.Config{
					MinVersion: tls.VersionTLS12,
				}
			}
			if len(tr.TLSClientConfig.Certificates) == 0 && tr.TLSClientConfig.GetClientCertificate == nil {
				tr.TLSClientConfig.Certificates = []tls.Certificate{o.certificate}
				tr.TLSClientConfig.GetClientCertificate = o.getClientCertificate
			}
		case *http2.Transport:
			if tr.TLSClientConfig == nil {
				tr.TLSClientConfig = &tls.Config{
					MinVersion: tls.VersionTLS12,
				}
			}
			if len(tr.TLSClientConfig.Certificates) == 0 && tr.TLSClientConfig.GetClientCertificate == nil {
				tr.TLSClientConfig.Certificates = []tls.Certificate{o.certificate}
				tr.TLSClientConfig.GetClientCertificate = o.getClientCertificate
			}
		default:
			return nil, errors.Errorf("unsupported transport type %T", tr)
		}
	}

	// Wrap the transport using the decorator function if necessary
	return decorateRoundTripper(tr, o.transportDecorator), nil
}

// WithTransport adds a custom transport to the Client. It will fail if a
// previous option to create the transport has been configured.
func WithTransport(tr http.RoundTripper) ClientOption {
	return func(o *clientOptions) error {
		if err := o.checkTransport(); err != nil {
			return err
		}
		o.transport = tr
		return nil
	}
}

// WithTransportDecorator allows customization of the HTTP transport used by the
// client. The provided function receives the configured [http.RoundTripper] and
// can wrap it with additional functionality.
func WithTransportDecorator(fn TransportDecorator) ClientOption {
	return func(o *clientOptions) error {
		o.transportDecorator = fn
		return nil
	}
}

// WithInsecure adds a insecure transport that bypasses TLS verification.
func WithInsecure() ClientOption {
	return func(o *clientOptions) error {
		o.transport = &http.Transport{
			Proxy: http.ProxyFromEnvironment,
			TLSClientConfig: &tls.Config{
				MinVersion: tls.VersionTLS12,
				//nolint:gosec // insecure option
				InsecureSkipVerify: true,
			},
		}
		return nil
	}
}

// WithRootFile will create the transport using the given root certificate. It
// will fail if a previous option to create the transport has been configured.
func WithRootFile(filename string) ClientOption {
	return func(o *clientOptions) error {
		if err := o.checkTransport(); err != nil {
			return err
		}
		o.rootFilename = filename
		return nil
	}
}

// WithRootSHA256 will create the transport using an insecure client to retrieve
// the root certificate using its fingerprint. It will fail if a previous option
// to create the transport has been configured.
func WithRootSHA256(sum string) ClientOption {
	return func(o *clientOptions) error {
		if err := o.checkTransport(); err != nil {
			return err
		}
		o.rootSHA256 = sum
		return nil
	}
}

// WithCABundle will create the transport using the given root certificates. It
// will fail if a previous option to create the transport has been configured.
func WithCABundle(bundle []byte) ClientOption {
	return func(o *clientOptions) error {
		if err := o.checkTransport(); err != nil {
			return err
		}
		o.rootBundle = bundle
		return nil
	}
}

// WithCertificate will set the given certificate as the TLS client certificate
// in the client.
func WithCertificate(cert tls.Certificate) ClientOption {
	return func(o *clientOptions) error {
		o.certificate = cert
		return nil
	}
}

// WithAdminX5C will set the given file as the X5C certificate for use
// by the client.
func WithAdminX5C(certs []*x509.Certificate, key interface{}, passwordFile string) ClientOption {
	return func(o *clientOptions) error {
		// Get private key from given key file
		var (
			err  error
			opts []jose.Option
		)
		if passwordFile != "" {
			opts = append(opts, jose.WithPasswordFile(passwordFile))
		}
		blk, err := pemutil.Serialize(key)
		if err != nil {
			return errors.Wrap(err, "error serializing private key")
		}
		o.x5cJWK, err = jose.ParseKey(pem.EncodeToMemory(blk), opts...)
		if err != nil {
			return err
		}
		o.x5cCertStrs, err = jose.ValidateX5C(certs, o.x5cJWK.Key)
		if err != nil {
			return errors.Wrap(err, "error validating x5c certificate chain and key for use in x5c header")
		}

		o.x5cCert = certs[0]
		switch leaf := certs[0]; {
		case leaf.Subject.CommonName != "":
			o.x5cSubject = leaf.Subject.CommonName
		case len(leaf.DNSNames) > 0:
			o.x5cSubject = leaf.DNSNames[0]
		case len(leaf.EmailAddresses) > 0:
			o.x5cSubject = leaf.EmailAddresses[0]
		}

		return nil
	}
}

// WithRetryFunc defines a method used to retry a request.
func WithRetryFunc(fn RetryFunc) ClientOption {
	return func(o *clientOptions) error {
		o.retryFunc = fn
		return nil
	}
}

// WithTimeout defines the time limit for requests made by this client. The
// timeout includes connection time, any redirects, and reading the response
// body.
func WithTimeout(d time.Duration) ClientOption {
	return func(o *clientOptions) error {
		o.timeout = d
		return nil
	}
}

func getTransportFromFile(filename string) (http.RoundTripper, error) {
	data, err := os.ReadFile(filename) // #nosec G703 -- filename is based on configuration; data read from file is processed with expected format
	if err != nil {
		return nil, errors.Wrapf(err, "error reading %s", filename)
	}
	pool := x509.NewCertPool()
	if !pool.AppendCertsFromPEM(data) {
		return nil, errors.Errorf("error parsing %s: no certificates found", filename)
	}
	return getDefaultTransport(&tls.Config{
		MinVersion:               tls.VersionTLS12,
		PreferServerCipherSuites: true,
		RootCAs:                  pool,
	}), nil
}

func getTransportFromSHA256(endpoint, sum string) (http.RoundTripper, error) {
	u, err := parseEndpoint(endpoint)
	if err != nil {
		return nil, err
	}
	caClient := &Client{endpoint: u}
	root, err := caClient.Root(sum)
	if err != nil {
		return nil, err
	}
	pool := x509.NewCertPool()
	pool.AddCert(root.RootPEM.Certificate)
	return getDefaultTransport(&tls.Config{
		MinVersion:               tls.VersionTLS12,
		PreferServerCipherSuites: true,
		RootCAs:                  pool,
	}), nil
}

func getTransportFromCABundle(bundle []byte) (http.RoundTripper, error) {
	pool := x509.NewCertPool()
	if !pool.AppendCertsFromPEM(bundle) {
		return nil, errors.New("error parsing ca bundle: no certificates found")
	}
	return getDefaultTransport(&tls.Config{
		MinVersion:               tls.VersionTLS12,
		PreferServerCipherSuites: true,
		RootCAs:                  pool,
	}), nil
}

// parseEndpoint parses and validates the given endpoint. It supports general
// URLs like https://ca.smallstep.com[:port][/path], and incomplete URLs like
// ca.smallstep.com[:port][/path].
func parseEndpoint(endpoint string) (*url.URL, error) {
	u, err := url.Parse(endpoint)
	if err != nil {
		return nil, errors.Wrapf(err, "error parsing endpoint '%s'", endpoint)
	}

	// URLs are generally parsed as:
	// [scheme:][//[userinfo@]host][/]path[?query][#fragment]
	// But URLs that do not start with a slash after the scheme are interpreted as
	// scheme:opaque[?query][#fragment]
	if u.Opaque == "" {
		if u.Scheme == "" {
			u.Scheme = "https"
		}
		if u.Host == "" {
			// endpoint looks like ca.smallstep.com or ca.smallstep.com/1.0/sign
			if u.Path != "" {
				parts := strings.SplitN(u.Path, "/", 2)
				u.Host = parts[0]
				if len(parts) == 2 {
					u.Path = parts[1]
				} else {
					u.Path = ""
				}
				return parseEndpoint(u.String())
			}
			return nil, errors.Errorf("error parsing endpoint: url '%s' is not valid", endpoint)
		}
		return u, nil
	}
	// scheme:opaque[?query][#fragment]
	// endpoint looks like ca.smallstep.com:443 or ca.smallstep.com:443/1.0/sign
	return parseEndpoint("https://" + endpoint)
}

// ProvisionerOption is the type of options passed to the Provisioner method.
type ProvisionerOption func(o *ProvisionerOptions) error

// ProvisionerOptions stores options for the provisioner CRUD API.
type ProvisionerOptions struct {
	Cursor string
	Limit  int
	ID     string
	Name   string
}

// Apply caches provisioner options on a struct for later use.
func (o *ProvisionerOptions) Apply(opts []ProvisionerOption) (err error) {
	for _, fn := range opts {
		if err = fn(o); err != nil {
			return
		}
	}
	return
}

func (o *ProvisionerOptions) rawQuery() string {
	v := url.Values{}
	if o.Cursor != "" {
		v.Set("cursor", o.Cursor)
	}
	if o.Limit > 0 {
		v.Set("limit", strconv.Itoa(o.Limit))
	}
	if o.ID != "" {
		v.Set("id", o.ID)
	}
	if o.Name != "" {
		v.Set("name", o.Name)
	}
	return v.Encode()
}

// WithProvisionerCursor will request the provisioners starting with the given cursor.
func WithProvisionerCursor(cursor string) ProvisionerOption {
	return func(o *ProvisionerOptions) error {
		o.Cursor = cursor
		return nil
	}
}

// WithProvisionerLimit will request the given number of provisioners.
func WithProvisionerLimit(limit int) ProvisionerOption {
	return func(o *ProvisionerOptions) error {
		o.Limit = limit
		return nil
	}
}

// WithProvisionerID will request the given provisioner.
func WithProvisionerID(id string) ProvisionerOption {
	return func(o *ProvisionerOptions) error {
		o.ID = id
		return nil
	}
}

// WithProvisionerName will request the given provisioner.
func WithProvisionerName(name string) ProvisionerOption {
	return func(o *ProvisionerOptions) error {
		o.Name = name
		return nil
	}
}

// Client implements an HTTP client for the CA server.
type Client struct {
	client             *uaClient
	endpoint           *url.URL
	retryFunc          RetryFunc
	timeout            time.Duration
	opts               []ClientOption
	transportDecorator TransportDecorator
}

// NewClient creates a new Client with the given endpoint and options.
func NewClient(endpoint string, opts ...ClientOption) (*Client, error) {
	u, err := parseEndpoint(endpoint)
	if err != nil {
		return nil, err
	}

	// Retrieve transport from options.
	o := defaultClientOptions()
	if err := o.apply(opts); err != nil {
		return nil, err
	}
	tr, err := o.getTransport(endpoint)
	if err != nil {
		return nil, err
	}

	return &Client{
		client:             newClient(tr, o.timeout),
		endpoint:           u,
		retryFunc:          o.retryFunc,
		timeout:            o.timeout,
		opts:               opts,
		transportDecorator: o.transportDecorator,
	}, nil
}

func (c *Client) retryOnError(r *http.Response) bool {
	if c.retryFunc != nil {
		if c.retryFunc(r.StatusCode) {
			o := defaultClientOptions()
			if err := o.apply(c.opts); err != nil {
				return false
			}
			tr, err := o.getTransport(c.endpoint.String())
			if err != nil {
				return false
			}
			r.Body.Close()
			c.client.SetTransport(tr)
			return true
		}
	}
	return false
}

// GetCaURL returns the configured CA url.
func (c *Client) GetCaURL() string {
	return c.endpoint.String()
}

// GetRootCAs returns the RootCAs certificate pool from the configured
// transport.
func (c *Client) GetRootCAs() *x509.CertPool {
	switch t := c.client.GetTransport().(type) {
	case *http.Transport:
		if t.TLSClientConfig != nil {
			return t.TLSClientConfig.RootCAs
		}
		return nil
	case *http2.Transport:
		if t.TLSClientConfig != nil {
			return t.TLSClientConfig.RootCAs
		}
		return nil
	default:
		return nil
	}
}

// SetTransport updates the transport of the internal HTTP client.
func (c *Client) SetTransport(tr http.RoundTripper) {
	c.client.SetTransport(tr)
}

// CloseIdleConnections closes any connections on its Transport which were
// previously connected from previous requests but are now sitting idle in a
// "keep-alive" state. It does not interrupt any connections currently in use.
func (c *Client) CloseIdleConnections() {
	c.client.CloseIdleConnections()
}

// Version performs the version request to the CA with an empty context and returns the
// api.VersionResponse struct.
func (c *Client) Version() (*api.VersionResponse, error) {
	return c.VersionWithContext(context.Background())
}

// VersionWithContext performs the version request to the CA with the provided context
// and returns the api.VersionResponse struct.
func (c *Client) VersionWithContext(ctx context.Context) (*api.VersionResponse, error) {
	var retried bool
	u := c.endpoint.ResolveReference(&url.URL{Path: "/version"})
retry:
	resp, err := c.client.GetWithContext(ctx, u.String())
	if err != nil {
		return nil, clientError(err)
	}
	if resp.StatusCode >= 400 {
		if !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context
			retried = true
			goto retry
		}
		return nil, readError(resp)
	}
	var version api.VersionResponse
	if err := readJSON(resp.Body, &version); err != nil {
		return nil, errs.Wrapf(http.StatusInternalServerError, err, "client.Version; error reading %s", u)
	}
	return &version, nil
}

// Health performs the health request to the CA with an empty context
// and returns the api.HealthResponse struct.
func (c *Client) Health() (*api.HealthResponse, error) {
	return c.HealthWithContext(context.Background())
}

// HealthWithContext performs the health request to the CA with the provided context
// and returns the api.HealthResponse struct.
func (c *Client) HealthWithContext(ctx context.Context) (*api.HealthResponse, error) {
	var retried bool
	u := c.endpoint.ResolveReference(&url.URL{Path: "/health"})
retry:
	resp, err := c.client.GetWithContext(ctx, u.String())
	if err != nil {
		return nil, clientError(err)
	}
	if resp.StatusCode >= 400 {
		if !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context
			retried = true
			goto retry
		}
		return nil, readError(resp)
	}
	var health api.HealthResponse
	if err := readJSON(resp.Body, &health); err != nil {
		return nil, errs.Wrapf(http.StatusInternalServerError, err, "client.Health; error reading %s", u)
	}
	return &health, nil
}

// Root performs the root request to the CA with an empty context and the provided
// SHA256 and returns the api.RootResponse struct. It uses an insecure client, but
// it checks the resulting root certificate with the given SHA256, returning an error
// if they do not match.
func (c *Client) Root(sha256Sum string) (*api.RootResponse, error) {
	return c.RootWithContext(context.Background(), sha256Sum)
}

// RootWithContext performs the root request to the CA with an empty context and the provided
// SHA256 and returns the api.RootResponse struct. It uses an insecure client, but
// it checks the resulting root certificate with the given SHA256, returning an error
// if they do not match.
func (c *Client) RootWithContext(ctx context.Context, sha256Sum string) (*api.RootResponse, error) {
	var retried bool
	sha256Sum = strings.ToLower(strings.ReplaceAll(sha256Sum, "-", ""))
	u := c.endpoint.ResolveReference(&url.URL{Path: "/root/" + sha256Sum})
retry:
	resp, err := newInsecureClient().GetWithContext(ctx, u.String())
	if err != nil {
		return nil, clientError(err)
	}
	if resp.StatusCode >= 400 {
		if !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context
			retried = true
			goto retry
		}
		return nil, readError(resp)
	}
	var root api.RootResponse
	if err := readJSON(resp.Body, &root); err != nil {
		return nil, errs.Wrapf(http.StatusInternalServerError, err, "client.Root; error reading %s", u)
	}
	// verify the sha256
	sum := sha256.Sum256(root.RootPEM.Raw)
	if !strings.EqualFold(sha256Sum, strings.ToLower(hex.EncodeToString(sum[:]))) {
		return nil, errs.BadRequest("root certificate fingerprint does not match")
	}
	return &root, nil
}

// Sign performs the sign request to the CA with an empty context and returns
// the api.SignResponse struct.
func (c *Client) Sign(req *api.SignRequest) (*api.SignResponse, error) {
	return c.SignWithContext(context.Background(), req)
}

// SignWithContext performs the sign request to the CA with the provided context
// and returns the api.SignResponse struct.
func (c *Client) SignWithContext(ctx context.Context, req *api.SignRequest) (*api.SignResponse, error) {
	var retried bool
	body, err := json.Marshal(req)
	if err != nil {
		return nil, errs.Wrap(http.StatusInternalServerError, err, "client.Sign; error marshaling request")
	}
	u := c.endpoint.ResolveReference(&url.URL{Path: "/sign"})
retry:
	resp, err := c.client.PostWithContext(ctx, u.String(), "application/json", bytes.NewReader(body))
	if err != nil {
		return nil, clientError(err)
	}
	if resp.StatusCode >= 400 {
		if !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context
			retried = true
			goto retry
		}
		return nil, readError(resp)
	}
	var sign api.SignResponse
	if err := readJSON(resp.Body, &sign); err != nil {
		return nil, errs.Wrapf(http.StatusInternalServerError, err, "client.Sign; error reading %s", u)
	}
	// Add tls.ConnectionState:
	// We'll extract the root certificate from the verified chains
	sign.TLS = resp.TLS
	return &sign, nil
}

// Renew performs the renew request to the CA with an empty context and
// returns the api.SignResponse struct.
func (c *Client) Renew(tr http.RoundTripper) (*api.SignResponse, error) {
	return c.RenewWithContext(context.Background(), tr)
}

// RenewWithContext performs the renew request to the CA with the provided context
// and returns the api.SignResponse struct.
func (c *Client) RenewWithContext(ctx context.Context, tr http.RoundTripper) (*api.SignResponse, error) {
	var retried bool
	u := c.endpoint.ResolveReference(&url.URL{Path: "/renew"})
	httpClient := &http.Client{Transport: tr}
retry:
	req, err := http.NewRequestWithContext(ctx, "POST", u.String(), http.NoBody)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", "application/json")
	resp, err := httpClient.Do(req)
	if err != nil {
		return nil, clientError(err)
	}
	if resp.StatusCode >= 400 {
		if !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context
			retried = true
			goto retry
		}
		return nil, readError(resp)
	}
	var sign api.SignResponse
	if err := readJSON(resp.Body, &sign); err != nil {
		return nil, errs.Wrapf(http.StatusInternalServerError, err, "client.Renew; error reading %s", u)
	}
	return &sign, nil
}

// RenewWithToken performs the renew request to the CA with the given
// authorization token and and empty context and returns the api.SignResponse struct.
// This method is generally used to renew an expired certificate.
func (c *Client) RenewWithToken(token string) (*api.SignResponse, error) {
	return c.RenewWithTokenAndContext(context.Background(), token)
}

// RenewWithTokenAndContext performs the renew request to the CA with the given
// authorization token and context and returns the api.SignResponse struct.
// This method is generally used to renew an expired certificate.
func (c *Client) RenewWithTokenAndContext(ctx context.Context, token string) (*api.SignResponse, error) {
	var retried bool
	u := c.endpoint.ResolveReference(&url.URL{Path: "/renew"})
	req, err := http.NewRequestWithContext(ctx, "POST", u.String(), http.NoBody)
	if err != nil {
		return nil, errors.Wrapf(err, "create POST %s request failed", u)
	}
	req.Header.Add("Authorization", "Bearer "+token)
retry:
	resp, err := c.client.Do(req)
	if err != nil {
		return nil, clientError(err)
	}
	if resp.StatusCode >= 400 {
		if !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context
			retried = true
			goto retry
		}
		return nil, readError(resp)
	}
	var sign api.SignResponse
	if err := readJSON(resp.Body, &sign); err != nil {
		return nil, errs.Wrapf(http.StatusInternalServerError, err, "client.RenewWithToken; error reading %s", u)
	}
	return &sign, nil
}

// Rekey performs the rekey request to the CA with an empty context and
// returns the api.SignResponse struct.
func (c *Client) Rekey(req *api.RekeyRequest, tr http.RoundTripper) (*api.SignResponse, error) {
	return c.RekeyWithContext(context.Background(), req, tr)
}

// RekeyWithContext performs the rekey request to the CA with the provided context
// and returns the api.SignResponse struct.
func (c *Client) RekeyWithContext(ctx context.Context, req *api.RekeyRequest, tr http.RoundTripper) (*api.SignResponse, error) {
	var retried bool
	body, err := json.Marshal(req)
	if err != nil {
		return nil, errors.Wrap(err, "error marshaling request")
	}
	u := c.endpoint.ResolveReference(&url.URL{Path: "/rekey"})
	httpClient := &http.Client{Transport: tr}
retry:
	httpReq, err := http.NewRequestWithContext(ctx, "POST", u.String(), bytes.NewReader(body))
	if err != nil {
		return nil, err
	}
	httpReq.Header.Set("Content-Type", "application/json")
	resp, err := httpClient.Do(httpReq)
	if err != nil {
		return nil, clientError(err)
	}
	if resp.StatusCode >= 400 {
		if !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context
			retried = true
			goto retry
		}
		return nil, readError(resp)
	}
	var sign api.SignResponse
	if err := readJSON(resp.Body, &sign); err != nil {
		return nil, errs.Wrapf(http.StatusInternalServerError, err, "client.Rekey; error reading %s", u)
	}
	return &sign, nil
}

// Revoke performs the revoke request to the CA with an empty context and returns
// the api.RevokeResponse struct.
func (c *Client) Revoke(req *api.RevokeRequest, tr http.RoundTripper) (*api.RevokeResponse, error) {
	return c.RevokeWithContext(context.Background(), req, tr)
}

// RevokeWithContext performs the revoke request to the CA with the provided context and
// returns the api.RevokeResponse struct.
func (c *Client) RevokeWithContext(ctx context.Context, req *api.RevokeRequest, tr http.RoundTripper) (*api.RevokeResponse, error) {
	var retried bool
	body, err := json.Marshal(req)
	if err != nil {
		return nil, errors.Wrap(err, "error marshaling request")
	}
	var uaClient *uaClient
retry:
	if tr != nil {
		uaClient = newClient(tr, c.timeout)
	} else {
		uaClient = c.client
	}

	u := c.endpoint.ResolveReference(&url.URL{Path: "/revoke"})
	resp, err := uaClient.PostWithContext(ctx, u.String(), "application/json", bytes.NewReader(body))
	if err != nil {
		return nil, clientError(err)
	}
	if resp.StatusCode >= 400 {
		if !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context
			retried = true
			goto retry
		}
		return nil, readError(resp)
	}
	var revoke api.RevokeResponse
	if err := readJSON(resp.Body, &revoke); err != nil {
		return nil, errors.Wrapf(err, "error reading %s", u)
	}
	return &revoke, nil
}

// Provisioners performs the provisioners request to the CA with an empty context
// and returns the api.ProvisionersResponse struct with a map of provisioners.
//
// ProvisionerOption WithProvisionerCursor and WithProvisionLimit can be used to
// paginate the provisioners.
func (c *Client) Provisioners(opts ...ProvisionerOption) (*api.ProvisionersResponse, error) {
	return c.ProvisionersWithContext(context.Background(), opts...)
}

// ProvisionersWithContext performs the provisioners request to the CA with the provided context
// and returns the api.ProvisionersResponse struct with a map of provisioners.
//
// ProvisionerOption WithProvisionerCursor and WithProvisionLimit can be used to
// paginate the provisioners.
func (c *Client) ProvisionersWithContext(ctx context.Context, opts ...ProvisionerOption) (*api.ProvisionersResponse, error) {
	var retried bool
	o := new(ProvisionerOptions)
	if err := o.Apply(opts); err != nil {
		return nil, err
	}
	u := c.endpoint.ResolveReference(&url.URL{
		Path:     "/provisioners",
		RawQuery: o.rawQuery(),
	})
retry:
	resp, err := c.client.GetWithContext(ctx, u.String())
	if err != nil {
		return nil, clientError(err)
	}
	if resp.StatusCode >= 400 {
		if !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context
			retried = true
			goto retry
		}
		return nil, readError(resp)
	}
	var provisioners api.ProvisionersResponse
	if err := readJSON(resp.Body, &provisioners); err != nil {
		return nil, errors.Wrapf(err, "error reading %s", u)
	}
	return &provisioners, nil
}

// ProvisionerKey performs the request to the CA with an empty context to get
// the encrypted key for the given provisioner kid and returns the api.ProvisionerKeyResponse
// struct with the encrypted key.
func (c *Client) ProvisionerKey(kid string) (*api.ProvisionerKeyResponse, error) {
	return c.ProvisionerKeyWithContext(context.Background(), kid)
}

// ProvisionerKeyWithContext performs the request to the CA with the provided context to get
// the encrypted key for the given provisioner kid and returns the api.ProvisionerKeyResponse
// struct with the encrypted key.
func (c *Client) ProvisionerKeyWithContext(ctx context.Context, kid string) (*api.ProvisionerKeyResponse, error) {
	var retried bool
	u := c.endpoint.ResolveReference(&url.URL{Path: "/provisioners/" + kid + "/encrypted-key"})
retry:
	resp, err := c.client.GetWithContext(ctx, u.String())
	if err != nil {
		return nil, clientError(err)
	}
	if resp.StatusCode >= 400 {
		if !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context
			retried = true
			goto retry
		}
		return nil, readError(resp)
	}
	var key api.ProvisionerKeyResponse
	if err := readJSON(resp.Body, &key); err != nil {
		return nil, errors.Wrapf(err, "error reading %s", u)
	}
	return &key, nil
}

// Roots performs the get roots request to the CA with an empty context
// and returns the api.RootsResponse struct.
func (c *Client) Roots() (*api.RootsResponse, error) {
	return c.RootsWithContext(context.Background())
}

// RootsWithContext performs the get roots request to the CA with the provided context
// and returns the api.RootsResponse struct.
func (c *Client) RootsWithContext(ctx context.Context) (*api.RootsResponse, error) {
	var retried bool
	u := c.endpoint.ResolveReference(&url.URL{Path: "/roots"})
retry:
	resp, err := c.client.GetWithContext(ctx, u.String())
	if err != nil {
		return nil, clientError(err)
	}
	if resp.StatusCode >= 400 {
		if !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context
			retried = true
			goto retry
		}
		return nil, readError(resp)
	}
	var roots api.RootsResponse
	if err := readJSON(resp.Body, &roots); err != nil {
		return nil, errors.Wrapf(err, "error reading %s", u)
	}
	return &roots, nil
}

// Federation performs the get federation request to the CA with an empty context
// and returns the api.FederationResponse struct.
func (c *Client) Federation() (*api.FederationResponse, error) {
	return c.FederationWithContext(context.Background())
}

// FederationWithContext performs the get federation request to the CA with the provided context
// and returns the api.FederationResponse struct.
func (c *Client) FederationWithContext(ctx context.Context) (*api.FederationResponse, error) {
	var retried bool
	u := c.endpoint.ResolveReference(&url.URL{Path: "/federation"})
retry:
	resp, err := c.client.GetWithContext(ctx, u.String())
	if err != nil {
		return nil, clientError(err)
	}
	if resp.StatusCode >= 400 {
		if !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context
			retried = true
			goto retry
		}
		return nil, readError(resp)
	}
	var federation api.FederationResponse
	if err := readJSON(resp.Body, &federation); err != nil {
		return nil, errors.Wrapf(err, "error reading %s", u)
	}
	return &federation, nil
}

// SSHSign performs the POST /ssh/sign request to the CA with an empty context
// and returns the api.SSHSignResponse struct.
func (c *Client) SSHSign(req *api.SSHSignRequest) (*api.SSHSignResponse, error) {
	return c.SSHSignWithContext(context.Background(), req)
}

// SSHSignWithContext performs the POST /ssh/sign request to the CA with the provided context
// and returns the api.SSHSignResponse struct.
func (c *Client) SSHSignWithContext(ctx context.Context, req *api.SSHSignRequest) (*api.SSHSignResponse, error) {
	var retried bool
	body, err := json.Marshal(req)
	if err != nil {
		return nil, errors.Wrap(err, "error marshaling request")
	}
	u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/sign"})
retry:
	resp, err := c.client.PostWithContext(ctx, u.String(), "application/json", bytes.NewReader(body))
	if err != nil {
		return nil, clientError(err)
	}
	if resp.StatusCode >= 400 {
		if !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context
			retried = true
			goto retry
		}
		return nil, readError(resp)
	}
	var sign api.SSHSignResponse
	if err := readJSON(resp.Body, &sign); err != nil {
		return nil, errors.Wrapf(err, "error reading %s", u)
	}
	return &sign, nil
}

// SSHRenew performs the POST /ssh/renew request to the CA with an empty context
// and returns the api.SSHRenewResponse struct.
func (c *Client) SSHRenew(req *api.SSHRenewRequest) (*api.SSHRenewResponse, error) {
	return c.SSHRenewWithContext(context.Background(), req)
}

// SSHRenewWithContext performs the POST /ssh/renew request to the CA with the provided context
// and returns the api.SSHRenewResponse struct.
func (c *Client) SSHRenewWithContext(ctx context.Context, req *api.SSHRenewRequest) (*api.SSHRenewResponse, error) {
	var retried bool
	body, err := json.Marshal(req)
	if err != nil {
		return nil, errors.Wrap(err, "error marshaling request")
	}
	u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/renew"})
retry:
	resp, err := c.client.PostWithContext(ctx, u.String(), "application/json", bytes.NewReader(body))
	if err != nil {
		return nil, clientError(err)
	}
	if resp.StatusCode >= 400 {
		if !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context
			retried = true
			goto retry
		}
		return nil, readError(resp)
	}
	var renew api.SSHRenewResponse
	if err := readJSON(resp.Body, &renew); err != nil {
		return nil, errors.Wrapf(err, "error reading %s", u)
	}
	return &renew, nil
}

// SSHRekey performs the POST /ssh/rekey request to the CA with an empty context
// and returns the api.SSHRekeyResponse struct.
func (c *Client) SSHRekey(req *api.SSHRekeyRequest) (*api.SSHRekeyResponse, error) {
	return c.SSHRekeyWithContext(context.Background(), req)
}

// SSHRekeyWithContext performs the POST /ssh/rekey request to the CA with the provided context
// and returns the api.SSHRekeyResponse struct.
func (c *Client) SSHRekeyWithContext(ctx context.Context, req *api.SSHRekeyRequest) (*api.SSHRekeyResponse, error) {
	var retried bool
	body, err := json.Marshal(req)
	if err != nil {
		return nil, errors.Wrap(err, "error marshaling request")
	}
	u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/rekey"})
retry:
	resp, err := c.client.PostWithContext(ctx, u.String(), "application/json", bytes.NewReader(body))
	if err != nil {
		return nil, clientError(err)
	}
	if resp.StatusCode >= 400 {
		if !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context
			retried = true
			goto retry
		}
		return nil, readError(resp)
	}
	var rekey api.SSHRekeyResponse
	if err := readJSON(resp.Body, &rekey); err != nil {
		return nil, errors.Wrapf(err, "error reading %s", u)
	}
	return &rekey, nil
}

// SSHRevoke performs the POST /ssh/revoke request to the CA with an empty context
// and returns the api.SSHRevokeResponse struct.
func (c *Client) SSHRevoke(req *api.SSHRevokeRequest) (*api.SSHRevokeResponse, error) {
	return c.SSHRevokeWithContext(context.Background(), req)
}

// SSHRevokeWithContext performs the POST /ssh/revoke request to the CA with the provided context
// and returns the api.SSHRevokeResponse struct.
func (c *Client) SSHRevokeWithContext(ctx context.Context, req *api.SSHRevokeRequest) (*api.SSHRevokeResponse, error) {
	var retried bool
	body, err := json.Marshal(req)
	if err != nil {
		return nil, errors.Wrap(err, "error marshaling request")
	}
	u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/revoke"})
retry:
	resp, err := c.client.PostWithContext(ctx, u.String(), "application/json", bytes.NewReader(body))
	if err != nil {
		return nil, clientError(err)
	}
	if resp.StatusCode >= 400 {
		if !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context
			retried = true
			goto retry
		}
		return nil, readError(resp)
	}
	var revoke api.SSHRevokeResponse
	if err := readJSON(resp.Body, &revoke); err != nil {
		return nil, errors.Wrapf(err, "error reading %s", u)
	}
	return &revoke, nil
}

// SSHRoots performs the GET /ssh/roots request to the CA with an empty context
// and returns the api.SSHRootsResponse struct.
func (c *Client) SSHRoots() (*api.SSHRootsResponse, error) {
	return c.SSHRootsWithContext(context.Background())
}

// SSHRootsWithContext performs the GET /ssh/roots request to the CA with the provided context
// and returns the api.SSHRootsResponse struct.
func (c *Client) SSHRootsWithContext(ctx context.Context) (*api.SSHRootsResponse, error) {
	var retried bool
	u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/roots"})
retry:
	resp, err := c.client.GetWithContext(ctx, u.String())
	if err != nil {
		return nil, clientError(err)
	}
	if resp.StatusCode >= 400 {
		if !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context
			retried = true
			goto retry
		}
		return nil, readError(resp)
	}
	var keys api.SSHRootsResponse
	if err := readJSON(resp.Body, &keys); err != nil {
		return nil, errors.Wrapf(err, "error reading %s", u)
	}
	return &keys, nil
}

// SSHFederation performs the get /ssh/federation request to the CA with an empty context
// and returns the api.SSHRootsResponse struct.
func (c *Client) SSHFederation() (*api.SSHRootsResponse, error) {
	return c.SSHFederationWithContext(context.Background())
}

// SSHFederationWithContext performs the get /ssh/federation request to the CA with the provided context
// and returns the api.SSHRootsResponse struct.
func (c *Client) SSHFederationWithContext(ctx context.Context) (*api.SSHRootsResponse, error) {
	var retried bool
	u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/federation"})
retry:
	resp, err := c.client.GetWithContext(ctx, u.String())
	if err != nil {
		return nil, clientError(err)
	}
	if resp.StatusCode >= 400 {
		if !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context
			retried = true
			goto retry
		}
		return nil, readError(resp)
	}
	var keys api.SSHRootsResponse
	if err := readJSON(resp.Body, &keys); err != nil {
		return nil, errors.Wrapf(err, "error reading %s", u)
	}
	return &keys, nil
}

// SSHConfig performs the POST /ssh/config request to the CA with an empty context
// to get the ssh configuration templates.
func (c *Client) SSHConfig(req *api.SSHConfigRequest) (*api.SSHConfigResponse, error) {
	return c.SSHConfigWithContext(context.Background(), req)
}

// SSHConfigWithContext performs the POST /ssh/config request to the CA with the provided context
// to get the ssh configuration templates.
func (c *Client) SSHConfigWithContext(ctx context.Context, req *api.SSHConfigRequest) (*api.SSHConfigResponse, error) {
	var retried bool
	body, err := json.Marshal(req)
	if err != nil {
		return nil, errors.Wrap(err, "error marshaling request")
	}
	u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/config"})
retry:
	resp, err := c.client.PostWithContext(ctx, u.String(), "application/json", bytes.NewReader(body))
	if err != nil {
		return nil, clientError(err)
	}
	if resp.StatusCode >= 400 {
		if !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context
			retried = true
			goto retry
		}
		return nil, readError(resp)
	}
	var cfg api.SSHConfigResponse
	if err := readJSON(resp.Body, &cfg); err != nil {
		return nil, errors.Wrapf(err, "error reading %s", u)
	}
	return &cfg, nil
}

// SSHCheckHost performs the POST /ssh/check-host request to the CA with an empty context,
// the principal and a token and returns the api.SSHCheckPrincipalResponse.
func (c *Client) SSHCheckHost(principal, token string) (*api.SSHCheckPrincipalResponse, error) {
	return c.SSHCheckHostWithContext(context.Background(), principal, token)
}

// SSHCheckHostWithContext performs the POST /ssh/check-host request to the CA with the provided context,
// principal and token and returns the api.SSHCheckPrincipalResponse.
func (c *Client) SSHCheckHostWithContext(ctx context.Context, principal, token string) (*api.SSHCheckPrincipalResponse, error) {
	var retried bool
	body, err := json.Marshal(&api.SSHCheckPrincipalRequest{
		Type:      provisioner.SSHHostCert,
		Principal: principal,
		Token:     token,
	})
	if err != nil {
		return nil, errs.Wrap(http.StatusInternalServerError, err, "error marshaling request",
			errs.WithMessage("Failed to marshal the check-host request"))
	}
	u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/check-host"})
retry:
	resp, err := c.client.PostWithContext(ctx, u.String(), "application/json", bytes.NewReader(body))
	if err != nil {
		return nil, clientError(err)
	}
	if resp.StatusCode >= 400 {
		if !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context
			retried = true
			goto retry
		}
		return nil, readError(resp)
	}
	var check api.SSHCheckPrincipalResponse
	if err := readJSON(resp.Body, &check); err != nil {
		return nil, errs.Wrapf(http.StatusInternalServerError, err, "error reading %s response",
			[]any{u, errs.WithMessage("Failed to parse response from /ssh/check-host endpoint")}...)
	}
	return &check, nil
}

// SSHGetHosts performs the GET /ssh/get-hosts request to the CA with an empty context.
func (c *Client) SSHGetHosts() (*api.SSHGetHostsResponse, error) {
	return c.SSHGetHostsWithContext(context.Background())
}

// SSHGetHostsWithContext performs the GET /ssh/get-hosts request to the CA with the provided context.
func (c *Client) SSHGetHostsWithContext(ctx context.Context) (*api.SSHGetHostsResponse, error) {
	var retried bool
	u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/hosts"})
retry:
	resp, err := c.client.GetWithContext(ctx, u.String())
	if err != nil {
		return nil, clientError(err)
	}
	if resp.StatusCode >= 400 {
		if !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context
			retried = true
			goto retry
		}
		return nil, readError(resp)
	}
	var hosts api.SSHGetHostsResponse
	if err := readJSON(resp.Body, &hosts); err != nil {
		return nil, errors.Wrapf(err, "error reading %s", u)
	}
	return &hosts, nil
}

// SSHBastion performs the POST /ssh/bastion request to the CA with an empty context.
func (c *Client) SSHBastion(req *api.SSHBastionRequest) (*api.SSHBastionResponse, error) {
	return c.SSHBastionWithContext(context.Background(), req)
}

// SSHBastionWithContext performs the POST /ssh/bastion request to the CA with the provided context.
func (c *Client) SSHBastionWithContext(ctx context.Context, req *api.SSHBastionRequest) (*api.SSHBastionResponse, error) {
	var retried bool
	body, err := json.Marshal(req)
	if err != nil {
		return nil, errors.Wrap(err, "client.SSHBastion; error marshaling request")
	}
	u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/bastion"})
retry:
	resp, err := c.client.PostWithContext(ctx, u.String(), "application/json", bytes.NewReader(body))
	if err != nil {
		return nil, clientError(err)
	}
	if resp.StatusCode >= 400 {
		if !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context
			retried = true
			goto retry
		}
		return nil, readError(resp)
	}
	var bastion api.SSHBastionResponse
	if err := readJSON(resp.Body, &bastion); err != nil {
		return nil, errors.Wrapf(err, "client.SSHBastion; error reading %s", u)
	}
	return &bastion, nil
}

// RootFingerprint is a helper method that returns the current root fingerprint.
// It does an health connection and gets the fingerprint from the TLS verified chains.
func (c *Client) RootFingerprint() (string, error) {
	return c.RootFingerprintWithContext(context.Background())
}

// RootFingerprintWithContext is a helper method that returns the current root fingerprint.
// It does an health connection and gets the fingerprint from the TLS verified chains.
func (c *Client) RootFingerprintWithContext(ctx context.Context) (string, error) {
	u := c.endpoint.ResolveReference(&url.URL{Path: "/health"})
	resp, err := c.client.GetWithContext(ctx, u.String())
	if err != nil {
		return "", clientError(err)
	}
	defer resp.Body.Close()
	if resp.TLS == nil || len(resp.TLS.VerifiedChains) == 0 {
		return "", errors.New("missing verified chains")
	}
	lastChain := resp.TLS.VerifiedChains[len(resp.TLS.VerifiedChains)-1]
	if len(lastChain) == 0 {
		return "", errors.New("missing verified chains")
	}
	return x509util.Fingerprint(lastChain[len(lastChain)-1]), nil
}

// CreateSignRequest is a helper function that given an x509 OTT returns a
// simple but secure sign request as well as the private key used.
func CreateSignRequest(ott string) (*api.SignRequest, crypto.PrivateKey, error) {
	token, err := jose.ParseSigned(ott)
	if err != nil {
		return nil, nil, errors.Wrap(err, "error parsing ott")
	}
	var claims authority.Claims
	if err := token.UnsafeClaimsWithoutVerification(&claims); err != nil {
		return nil, nil, errors.Wrap(err, "error parsing ott")
	}

	pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	if err != nil {
		return nil, nil, errors.Wrap(err, "error generating key")
	}

	dnsNames, ips, emails, uris := x509util.SplitSANs(claims.SANs)
	if claims.Email != "" {
		emails = append(emails, claims.Email)
	}

	template := &x509.CertificateRequest{
		Subject: pkix.Name{
			CommonName: claims.Subject,
		},
		SignatureAlgorithm: x509.ECDSAWithSHA256,
		DNSNames:           dnsNames,
		IPAddresses:        ips,
		EmailAddresses:     emails,
		URIs:               uris,
	}

	csr, err := x509.CreateCertificateRequest(rand.Reader, template, pk)
	if err != nil {
		return nil, nil, errors.Wrap(err, "error creating certificate request")
	}
	cr, err := x509.ParseCertificateRequest(csr)
	if err != nil {
		return nil, nil, errors.Wrap(err, "error parsing certificate request")
	}
	if err := cr.CheckSignature(); err != nil {
		return nil, nil, errors.Wrap(err, "error signing certificate request")
	}
	return &api.SignRequest{
		CsrPEM: api.CertificateRequest{CertificateRequest: cr},
		OTT:    ott,
	}, pk, nil
}

// CreateCertificateRequest creates a new CSR with the given common name and
// SANs. If no san is provided the commonName will set also a SAN.
func CreateCertificateRequest(commonName string, sans ...string) (*api.CertificateRequest, crypto.PrivateKey, error) {
	key, err := keyutil.GenerateDefaultKey()
	if err != nil {
		return nil, nil, err
	}
	return createCertificateRequest(commonName, sans, key)
}

// CreateIdentityRequest returns a new CSR to create the identity. If an
// identity was already present it reuses the private key.
func CreateIdentityRequest(commonName string, sans ...string) (*api.CertificateRequest, crypto.PrivateKey, error) {
	var identityKey crypto.PrivateKey
	if i, err := identity.LoadDefaultIdentity(); err == nil && i.Key != "" {
		if k, err := pemutil.Read(i.Key); err == nil {
			identityKey = k
		}
	}
	if identityKey == nil {
		return CreateCertificateRequest(commonName, sans...)
	}
	return createCertificateRequest(commonName, sans, identityKey)
}

// LoadDefaultIdentity is a wrapper for identity.LoadDefaultIdentity.
func LoadDefaultIdentity() (*identity.Identity, error) {
	return identity.LoadDefaultIdentity()
}

// WriteDefaultIdentity is a wrapper for identity.WriteDefaultIdentity.
func WriteDefaultIdentity(certChain []api.Certificate, key crypto.PrivateKey) error {
	return identity.WriteDefaultIdentity(certChain, key)
}

func createCertificateRequest(commonName string, sans []string, key crypto.PrivateKey) (*api.CertificateRequest, crypto.PrivateKey, error) {
	if len(sans) == 0 {
		sans = []string{commonName}
	}
	dnsNames, ips, emails, uris := x509util.SplitSANs(sans)
	template := &x509.CertificateRequest{
		Subject: pkix.Name{
			CommonName: commonName,
		},
		DNSNames:       dnsNames,
		IPAddresses:    ips,
		EmailAddresses: emails,
		URIs:           uris,
	}
	csr, err := x509.CreateCertificateRequest(rand.Reader, template, key)
	if err != nil {
		return nil, nil, err
	}
	cr, err := x509.ParseCertificateRequest(csr)
	if err != nil {
		return nil, nil, err
	}
	if err := cr.CheckSignature(); err != nil {
		return nil, nil, err
	}

	return &api.CertificateRequest{CertificateRequest: cr}, key, nil
}

// getRootCAPath returns the path where the root CA is stored based on the
// STEPPATH environment variable.
func getRootCAPath() string {
	return filepath.Join(step.Path(), "certs", "root_ca.crt")
}

func readJSON(r io.ReadCloser, v interface{}) error {
	defer r.Close()
	return json.NewDecoder(r).Decode(v)
}

func readProtoJSON(r io.ReadCloser, m proto.Message) error {
	defer r.Close()
	data, err := io.ReadAll(r)
	if err != nil {
		return err
	}
	return protojson.Unmarshal(data, m)
}

func readError(r *http.Response) error {
	defer r.Body.Close()
	apiErr := new(errs.Error)
	if err := json.NewDecoder(r.Body).Decode(apiErr); err != nil {
		return fmt.Errorf("failed decoding CA error response: %w", err)
	}
	apiErr.RequestID = r.Header.Get("X-Request-Id")
	return apiErr
}

func clientError(err error) error {
	var uerr *url.Error
	if errors.As(err, &uerr) {
		return fmt.Errorf("client %s %s failed: %w",
			strings.ToUpper(uerr.Op), uerr.URL, uerr.Err)
	}
	return fmt.Errorf("client request failed: %w", err)
}

func decorateRoundTripper(tr http.RoundTripper, td TransportDecorator) http.RoundTripper {
	if td != nil {
		return td(tr)
	}
	return tr
}
