Как создать тестовый сервер, который использует аутентификацию клиента TLS в Go? - PullRequest
0 голосов
/ 07 апреля 2020

Я хотел бы написать модульный тест для обработчика HTTP, который извлекает определенную информацию из сертификата устройства. Я нашел этот гист, https://gist.github.com/ncw/9253562, который использует openssl для генерации сертификатов и просто читает полученные файлы в его client.go и server.go. Однако, чтобы сделать вещи немного более прозрачными, я бы хотел сгенерировать сертификаты, используя стандартную библиотеку Go.

Вот моя попытка пока что провести модульное тестирование (доступно на 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
}

Однако, когда я запускаю его, я получаю следующую ошибку:

> 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

Я не совсем уверен, что делать с ошибкой сообщение

не может проверить сертификат для 127.0.0.1, поскольку он не содержит IP-адресов SAN

, поскольку при создании сертификата я передаю поле IPAddresses. Есть идеи, что тут не так?

Ответы [ 3 ]

1 голос
/ 07 апреля 2020

Ошибка связана с расширением поля SAN, присутствующим в сертификате X509. Поле SAN в сертификате X509 может содержать записи следующих типов:

  1. DNS-имя
  2. IP-адрес
  3. URI

Подробности могут здесь

Обычно в процессе проверки сертификата проверка расширения SAN может выполняться в некоторых системах. Следовательно, вы видите такое сообщение об ошибке

У вас есть два способа избежать этого сообщения об ошибке:

  1. Добавить поле SAN IP в свой сертификат
  2. Пропустить шаг подтверждения сертификата [ Это то, что вы сделали, комментируя]
1 голос
/ 07 апреля 2020

Я не совсем уверен, что делать с сообщением об ошибке

не может проверить сертификат для 127.0.0.1, поскольку он не содержит IP-адресов SAN

потому что я передаю в поле IPAddresses при создании сертификата. Любые идеи о том, что здесь не так?

Проблема в том, что вы передаете в поле IPAddresses при создании сертификата client , но не при создается сертификат server , поскольку ваш сервер просто использует сертификат CA как собственный, а сертификат CA (правильно) не содержит IP-адрес, поэтому сообщение об ошибке является правильным:

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

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

Вы должны создать сертификат сервера, подписанный ЦС (или a ЦС), так же, как вы создаете сертификат клиента и использовать его для своего тестового сервера. .

Как правило, наличие одного ключа выполняет двойную функцию в качестве ЦС и TLS-сервера так, как вы это делаете, что создает проблемы, и здесь нет веских причин делать это; в то время как RFC5280 на самом деле не запрещает эту практику, по крайней мере, кажется, что она не поощряет ее, если этого не требуют особые обстоятельства.

Однако в нынешнем виде способ использования сертификата CA технически не соответствует RFC5280, поскольку он содержит расширение использования расширенного ключа, определяющее только аутентификацию клиента и сервера TLS, но вы используете его для подписи сертификатов. Это, вероятно, допустимо, но в отсутствие ключевой цели anyExtendedKeyUsage 1026 * здесь действительно не должно быть.

0 голосов
/ 07 апреля 2020

Посмотрев немного более подробно на суть ncw, я заметил, что одним из ключевых отличий было значение параметра InsecureSkipVerify в конфигурации клиента TLS на true. Я добавил это, поэтому

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

, и теперь тест проходит.

Проверка сертификата сервера выходит за рамки этого теста, так что этого достаточно для моих целей, но я бы по-прежнему интересно узнать, почему проверка не удалась.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...