1

我想为从设备证书中提取某些信息的 HTTP 处理程序编写一个单元测试。我找到了这个要点https://gist.github.com/ncw/9253562,它用于openssl生成证书并简单地读取其client.goserver.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创建证书时正在现场传递。关于这里有什么问题的任何想法?

4

3 回答 3

1

该错误与 X509 证书中存在的 SAN 字段扩展有关。X509 证书中的 SAN 字段可以包含以下类型的条目;

  1. DNS 名称
  2. IP地址
  3. URI

详细信息可以在这里找到

通常在证书验证过程中,可以在某些系统上执行 SAN 扩展验证。因此,您会看到这样的错误消息

您有两个选项可以避免此错误消息,

  1. 在证书中添加 SAN IP 字段
  2. 跳过证书验证步骤 [这是您通过注释掉所做的]
于 2020-04-07T12:34:36.503 回答
1

我不确定如何处理错误消息

无法验证 127.0.0.1 的证书,因为它不包含任何 IP SAN

因为我在创建证书时传入了 IPAddresses 字段。关于这里有什么问题的任何想法?

问题是您在创建客户端IPAddresses证书时传递了该字段,而不是在创建服务器证书时传递,因为您的服务器只是将 CA 证书用作自己的证书,而 CA 证书(正确) 不包括 IP 地址,因此错误消息是正确的:

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

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

您应该创建一个由 CA(或 CA)签名的服务器证书与创建客户端证书的方式相同,并将其用于您的测试服务器。

通常,以您正在做的方式让单个密钥作为 CA 和作为 TLS 服务器执行双重职责是自找麻烦,在这里没有充分的理由这样做;虽然 RFC5280 实际上并没有禁止这种做法,但它至少似乎不鼓励这种做法,除非有特殊情况需要。

不过,就目前而言,您使用 CA 证书的方式在技术上不符合 RFC5280,因为它包含一个扩展的密钥使用扩展,仅指定 TLS 客户端和服务器身份验证,但您使用它来签署证书。它可能是宽容的,但在没有anyExtendedKeyUsage关键目的的情况下x509.CreateCertificate,这里真的应该失败。

于 2020-04-07T16:05:20.357 回答
-1

仔细观察ncw's gist,我注意到一个关键的区别是InsecureSkipVerify客户端的 TLS 配置中的选项设置为true. 我添加了这个,所以

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

现在测试通过了。

服务器证书的验证超出了这个测试的范围,所以这对我的目的来说已经足够了,但我仍然很想知道为什么验证失败。

于 2020-04-07T00:24:26.813 回答