Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 22 additions & 23 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,11 @@ func (c *Client) CACerts(ctx context.Context) ([]*x509.Certificate, error) {
return nil, err
}

httpc, _, err := c.makeHTTPClient()
httpc, conn, err := c.makeHTTPClient(ctx)
if err != nil {
return nil, err
}
defer conn.Close()

resp, err := httpc.Do(req)
if err != nil {
Expand All @@ -153,10 +154,11 @@ func (c *Client) CSRAttrs(ctx context.Context) (CSRAttrs, error) {
return CSRAttrs{}, err
}

httpc, _, err := c.makeHTTPClient()
httpc, conn, err := c.makeHTTPClient(ctx)
if err != nil {
return CSRAttrs{}, err
}
defer conn.Close()

resp, err := httpc.Do(req)
if err != nil {
Expand Down Expand Up @@ -205,14 +207,18 @@ func (c *Client) enrollCommon(ctx context.Context, r *x509.CertificateRequest, r
endpoint = reenrollEndpoint
}

httpc, tlsUnique64, err := c.makeHTTPClient()
httpc, conn, err := c.makeHTTPClient(ctx)
if err != nil {
return nil, err
}
defer conn.Close()

// RFC 7030 section 3.5 recommends including the tls-unique value in the
// CSR challenge password. It is not available with TLS 1.3.
// See https://pkg.go.dev/crypto/tls#ConnectionState.TLSUnique.
crBs := r.Raw
if tlsUnique64 != "" && c.SigningKey != nil {
crBs, err = c.addChallengePassword(r.Raw, tlsUnique64)
if tlsu := conn.ConnectionState().TLSUnique; tlsu != nil && c.SigningKey != nil {
crBs, err = c.addChallengePassword(r.Raw, string(base64Encode(tlsu)))
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -251,10 +257,11 @@ func (c *Client) ServerKeyGen(ctx context.Context, r *x509.CertificateRequest) (
return nil, nil, err
}

httpc, _, err := c.makeHTTPClient()
httpc, conn, err := c.makeHTTPClient(ctx)
if err != nil {
return nil, nil, err
}
defer conn.Close()

resp, err := httpc.Do(req)
if err != nil {
Expand Down Expand Up @@ -382,10 +389,11 @@ func (c *Client) TPMEnroll(
return nil, nil, nil, err
}

httpc, _, err := c.makeHTTPClient()
httpc, conn, err := c.makeHTTPClient(ctx)
if err != nil {
return nil, nil, nil, err
}
defer conn.Close()

resp, err := httpc.Do(req)
if err != nil {
Expand Down Expand Up @@ -598,12 +606,8 @@ func (c *Client) uri(endpoint string) string {
}

// makeHTTPClient makes and configures an HTTP client for connecting to an
// EST server. It also returns the corresponding TLS-unique value base64 encoded which could be required by EST server.
//
// RFC 7030 - section 3.5 recommends including it in the CSR challenge password field.
//
// The value could be nil with respect to TLS version. More details at https://pkg.go.dev/crypto/tls#ConnectionState.TLSUnique
func (c *Client) makeHTTPClient() (*http.Client, string, error) {
// EST server. It also returns the TLS connection, which the caller must close.
func (c *Client) makeHTTPClient(ctx context.Context) (*http.Client, *tls.Conn, error) {
var rootCAs *x509.CertPool
if c.ExplicitAnchor != nil {
rootCAs = c.ExplicitAnchor
Expand All @@ -625,17 +629,12 @@ func (c *Client) makeHTTPClient() (*http.Client, string, error) {
InsecureSkipVerify: c.InsecureSkipVerify,
}

conn, err := tls.Dial(tcpProtocol, c.Host, tlsClientConfig)
dialer := &tls.Dialer{Config: tlsClientConfig}
nc, err := dialer.DialContext(ctx, tcpProtocol, c.Host)
if err != nil {
return nil, "", err
}

var tlsUnique64 string
if tlsu := conn.ConnectionState().TLSUnique; tlsu != nil {
tlsUnique64 = string(base64Encode(tlsu))
} else {
tlsUnique64 = ""
return nil, nil, err
}
conn := nc.(*tls.Conn)

httpc := &http.Client{
Transport: &http.Transport{
Expand All @@ -646,5 +645,5 @@ func (c *Client) makeHTTPClient() (*http.Client, string, error) {
},
}

return httpc, tlsUnique64, nil
return httpc, conn, nil
}
64 changes: 64 additions & 0 deletions est_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import (
"reflect"
"strconv"
"strings"
"sync/atomic"
"testing"
"time"

Expand Down Expand Up @@ -150,6 +151,69 @@ func TestCSRAttrs(t *testing.T) {
}
}

func TestClientContextDeadline(t *testing.T) {
t.Parallel()

// The listener never accepts, so the TLS handshake never completes.
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("failed to listen: %v", err)
}
defer ln.Close()

ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()

client := est.Client{Host: ln.Addr().String(), InsecureSkipVerify: true}

errc := make(chan error, 1)
go func() {
_, err := client.CACerts(ctx)
errc <- err
}()

select {
case err := <-errc:
if !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("got error %v, want %v", err, context.DeadlineExceeded)
}
case <-time.After(testTimeout):
t.Fatal("CACerts did not return after the context deadline")
}
}

func TestClientClosesConnections(t *testing.T) {
t.Parallel()

var open int64
s := httptest.NewUnstartedServer(http.NotFoundHandler())
s.Config.ConnState = func(_ net.Conn, state http.ConnState) {
switch state {
case http.StateNew:
atomic.AddInt64(&open, 1)
case http.StateClosed:
atomic.AddInt64(&open, -1)
}
}
s.StartTLS()
defer s.Close()

client := est.Client{Host: strings.TrimPrefix(s.URL, "https://"), InsecureSkipVerify: true}
for i := 0; i < 3; i++ {
if _, err := client.CACerts(context.Background()); err == nil {
t.Fatal("CACerts unexpectedly succeeded")
}
}

deadline := time.Now().Add(testTimeout)
for atomic.LoadInt64(&open) != 0 {
if time.Now().After(deadline) {
t.Fatalf("%d connections still open", atomic.LoadInt64(&open))
}
time.Sleep(10 * time.Millisecond)
}
}

func TestEnroll(t *testing.T) {
t.Parallel()

Expand Down