How to create a test server which uses TLS client authentication in Go?

2.6k Views Asked by At

I'd like to write a unit test for an HTTP handler which extracts certain information from a device's certificate. I've found this gist, https://gist.github.com/ncw/9253562, which uses openssl to generate the certificates and simply reads the resulting files in its client.go and server.go. To make things a bit more transparent, however, I'd like to generate the certificates using Go's standard library.

Here is my attempt so far at the unit test (available at https://github.com/kurtpeek/client-auth-test):

package main

import (
    "crypto"
    "crypto/rand"
    "crypto/rsa"
    "crypto/sha1"
    "crypto/tls"
    "crypto/x509"
    "crypto/x509/pkix"
    "encoding/asn1"
    "encoding/pem"
    "io"
    "math/big"
    "net"
    "net/http"
    "net/http/httptest"
    "testing"
    "time"

    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)

func TestDeviceFromTLS(t *testing.T) {
    deviceKeyPEM, csrPEM := generateKeyAndCSR(t)

    caKey, caKeyPEM := generateKey(t)
    caCert, caCertPEM := generateRootCert(t, caKey)

    deviceCertPEM := signCSR(t, csrPEM, caKey, caCert)

    serverCert, err := tls.X509KeyPair(caCertPEM, caKeyPEM)
    require.NoError(t, err)

    clientPool := x509.NewCertPool()
    clientPool.AddCert(caCert)

    ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        assert.Len(t, r.TLS.PeerCertificates, 1)
    }))
    ts.TLS = &tls.Config{
        Certificates: []tls.Certificate{serverCert},
        ClientAuth:   tls.RequireAndVerifyClientCert,
        ClientCAs:    clientPool,
    }
    ts.StartTLS()
    defer ts.Close()

    deviceCert, err := tls.X509KeyPair(deviceCertPEM, deviceKeyPEM)
    require.NoError(t, err)

    pool := x509.NewCertPool()
    pool.AddCert(caCert)

    client := ts.Client()
    client.Transport.(*http.Transport).TLSClientConfig = &tls.Config{
        Certificates: []tls.Certificate{deviceCert},
        RootCAs:      pool,
    }

    req, err := http.NewRequest(http.MethodPut, ts.URL, nil)
    resp, err := client.Do(req)
    require.NoError(t, err)
    defer resp.Body.Close()

    assert.Exactly(t, http.StatusOK, resp.StatusCode)
}

func generateKeyAndCSR(t *testing.T) ([]byte, []byte) {
    rsaKey, err := rsa.GenerateKey(rand.Reader, 1024)
    require.NoError(t, err)

    key := pem.EncodeToMemory(&pem.Block{
        Type:  "RSA PRIVATE KEY",
        Bytes: x509.MarshalPKCS1PrivateKey(rsaKey),
    })

    template := &x509.CertificateRequest{
        Subject: pkix.Name{
            Country:      []string{"US"},
            Locality:     []string{"San Francisco"},
            Organization: []string{"Awesomeness, Inc."},
            Province:     []string{"California"},
        },
        SignatureAlgorithm: x509.SHA256WithRSA,
        IPAddresses:        []net.IP{net.ParseIP("127.0.0.1")},
    }

    req, err := x509.CreateCertificateRequest(rand.Reader, template, rsaKey)
    require.NoError(t, err)

    csr := pem.EncodeToMemory(&pem.Block{
        Type:  "CERTIFICATE REQUEST",
        Bytes: req,
    })

    return key, csr
}

func generateRootCert(t *testing.T, key crypto.Signer) (*x509.Certificate, []byte) {
    subjectKeyIdentifier := calculateSubjectKeyIdentifier(t, key.Public())

    template := &x509.Certificate{
        SerialNumber: generateSerial(t),
        Subject: pkix.Name{
            Organization: []string{"Awesomeness, Inc."},
            Country:      []string{"US"},
            Locality:     []string{"San Francisco"},
        },
        NotBefore:             time.Now(),
        NotAfter:              time.Now().AddDate(10, 0, 0),
        SubjectKeyId:          subjectKeyIdentifier,
        AuthorityKeyId:        subjectKeyIdentifier,
        KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
        ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
        BasicConstraintsValid: true,
        IsCA:                  true,
        MaxPathLenZero:        true,
    }

    der, err := x509.CreateCertificate(rand.Reader, template, template, key.Public(), key)
    require.NoError(t, err)

    rootCert, err := x509.ParseCertificate(der)
    require.NoError(t, err)

    rootCertPEM := pem.EncodeToMemory(&pem.Block{
        Type:  "CERTIFICATE",
        Bytes: der,
    })

    return rootCert, rootCertPEM
}

// generateSerial generates a serial number using the maximum number of octets (20) allowed by RFC 5280 4.1.2.2
// (Adapted from https://github.com/cloudflare/cfssl/blob/828c23c22cbca1f7632b9ba85174aaa26e745340/signer/local/local.go#L407-L418)
func generateSerial(t *testing.T) *big.Int {
    serialNumber := make([]byte, 20)
    _, err := io.ReadFull(rand.Reader, serialNumber)
    require.NoError(t, err)

    return new(big.Int).SetBytes(serialNumber)
}

// calculateSubjectKeyIdentifier implements a common method to generate a key identifier
// from a public key, namely, by composing it from the 160-bit SHA-1 hash of the bit string
// of the public key (cf. https://tools.ietf.org/html/rfc5280#section-4.2.1.2).
// (Adapted from https://github.com/jsha/minica/blob/master/main.go).
func calculateSubjectKeyIdentifier(t *testing.T, pubKey crypto.PublicKey) []byte {
    spkiASN1, err := x509.MarshalPKIXPublicKey(pubKey)
    require.NoError(t, err)

    var spki struct {
        Algorithm        pkix.AlgorithmIdentifier
        SubjectPublicKey asn1.BitString
    }
    _, err = asn1.Unmarshal(spkiASN1, &spki)
    require.NoError(t, err)

    skid := sha1.Sum(spki.SubjectPublicKey.Bytes)
    return skid[:]
}

// signCSR signs a certificate signing request with the given CA certificate and private key
func signCSR(t *testing.T, csr []byte, caKey crypto.Signer, caCert *x509.Certificate) []byte {
    block, _ := pem.Decode(csr)
    require.NotNil(t, block, "failed to decode CSR")

    certificateRequest, err := x509.ParseCertificateRequest(block.Bytes)
    require.NoError(t, err)

    require.NoError(t, certificateRequest.CheckSignature())

    template := x509.Certificate{
        Subject:               certificateRequest.Subject,
        PublicKeyAlgorithm:    certificateRequest.PublicKeyAlgorithm,
        PublicKey:             certificateRequest.PublicKey,
        SignatureAlgorithm:    certificateRequest.SignatureAlgorithm,
        Signature:             certificateRequest.Signature,
        SerialNumber:          generateSerial(t),
        Issuer:                caCert.Issuer,
        NotBefore:             time.Now(),
        NotAfter:              time.Now().AddDate(10, 0, 0),
        KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
        ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
        SubjectKeyId:          calculateSubjectKeyIdentifier(t, certificateRequest.PublicKey),
        BasicConstraintsValid: true,
        IPAddresses:           certificateRequest.IPAddresses,
    }

    derBytes, err := x509.CreateCertificate(rand.Reader, &template, caCert, certificateRequest.PublicKey, caKey)
    require.NoError(t, err)

    return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
}

// generateKey generates a 1024-bit RSA private key
func generateKey(t *testing.T) (crypto.Signer, []byte) {
    key, err := rsa.GenerateKey(rand.Reader, 1024)
    require.NoError(t, err)

    keyPEM := pem.EncodeToMemory(&pem.Block{
        Type:  "RSA PRIVATE KEY",
        Bytes: x509.MarshalPKCS1PrivateKey(key),
    })

    return key, keyPEM
}

However, when I run it, I get the following error:

> go test ./...
2020/04/06 15:12:30 http: TLS handshake error from 127.0.0.1:58685: remote error: tls: bad certificate
--- FAIL: TestDeviceFromTLS (0.05s)
    main_test.go:64: 
            Error Trace:    main_test.go:64
            Error:          Received unexpected error:
                            Put https://127.0.0.1:58684: x509: cannot validate certificate for 127.0.0.1 because it doesn't contain any IP SANs
            Test:           TestDeviceFromTLS
FAIL
FAIL    github.com/kurtpeek/client-auth-test    0.379s

I'm not really sure what to make of the error message

cannot validate certificate for 127.0.0.1 because it doesn't contain any IP SANs

because I am passing in the IPAddresses field when creating the certificate. Any ideas on what is wrong here?

3

There are 3 best solutions below

2
On BEST ANSWER

I'm not really sure what to make of the error message

cannot validate certificate for 127.0.0.1 because it doesn't contain any IP SANs

because I am passing in the IPAddresses field when creating the certificate. Any ideas on what is wrong here?

The problem is that you're passing in the IPAddresses field when you're creating the client certificate, but not when you're creating the server certificate, because your server is just using the CA certificate as its own, and the CA certificate (correctly) doesn't include an IP address, so the error message is therefore correct:

caKey, caKeyPEM := generateKey(t)
caCert, caCertPEM := generateRootCert(t, caKey)

serverCert, err := tls.X509KeyPair(caCertPEM, caKeyPEM)

You should be creating a server certificate signed by the CA (or by a CA), in the same way that you create the client certificate, and using that for your test server.

In general, having a single key perform double duty as a CA and as a TLS server in the way that you're doing is asking for trouble, and there's no good reason to do it here; while RFC5280 doesn't actually forbid the practice, it at least seems to discourage it unless special circumstances require it.

As it stands, though, the way you're using your CA certificate is technically non-compliant with RFC5280 since it contains an extended key usage extension specifying only TLS client and server authentication, but you're using it to sign certificates. It's probably being permissive, but in the absence of the anyExtendedKeyUsage key purpose x509.CreateCertificate really ought to be failing, here.

1
On

Looking a bit more closely at ncw's gist, I noticed that one key difference was the setting of the InsecureSkipVerify option in the client's TLS config to true. I added this, so

    client.Transport.(*http.Transport).TLSClientConfig = &tls.Config{
        Certificates:       []tls.Certificate{deviceCert},
        RootCAs:            pool,
        InsecureSkipVerify: true,
    }

and now the test passes.

Verification of the server certificate is beyond the scope of this test, so this is sufficient for my purposes, but I would still be curious to learn why the verification was failing.

2
On

The Error is related to the SAN field extension present in X509 certificate. A SAN field in X509 certificate can contain following type of entries;

  1. DNS Name
  2. IP Address
  3. URI

Details can be found here

Usually during certificate verification process the SAN extension validation could be performed on some systems. Hence you see such error message

You have two option to avoid this error message,

  1. Add a SAN IP field in your certificate
  2. Skip Certificate Verification Step [ This is what you have done by commenting out ]