TLS with Go

TLS with Go

原文见:https://ericchiang.github.io/post/go-tls/

虽然之前也接触一些 openssl 的编程,但是对证书颁发,证书链的一些细节依然有些似懂非懂。完成这篇文章之后解决了之前的很多困惑,果然实践动手是加固理论的最好方式。

下面的实践是基于Go的TLS,但是搞懂之后别的语言也一样的。

在阅读本文之前,需要先了解以下知识:
数字签名
证书链

公私钥的加解密

先从最基本的公钥和私钥的加密开始,由私钥加密的数据,只有公钥可以解开。go 的 crypto/rsa 包里直接提供了生成公钥和私钥的方法。

以下是生成一对公钥和私钥,其中公钥存在 privKey.PublicKey 里

// create a public/private keypair
// NOTE: Use crypto/rand not math/rand
privKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
    log.Fatalf("generating random key: %v", err)
}

接下来,我们使用生成的公钥对一些数据进行加密

plainText := []byte("hello world")
// use the public key to encrypt the message
cipherText, err := rsa.EncryptPKCS1v15(rand.Reader, &privKey.PublicKey, plainText)
if err != nil {
    log.Fatalf("could not encrypt data: %v", err)
}
log.Printf("%s\n", strconv.Quote(string(cipherText)))

观察输出,加密后的数据已经是不可读的了。接下来,用私钥对加密数据进行解密

decryptedText, err := rsa.DecryptPKCS1v15(nil, privKey, cipherText)
if err != nil {
    log.Fatalf("error decrypting cipher text: %v", err)
}
log.Printf("%s\n", decryptedText)

可以看到,数据被正常的解密之后,就可以看到了明文了。

这是网络中数据安全传输最简单也最基础的一步,通过加密数据,我们可以防止传输过程中数据明文被人获取。

数字签名

公私钥除了加解密之外的另一个应用是,对给定的信息创建一个数字签名。这些签名可以保证被签名文件的有效性,也就是没有被修改过。

具体的做法,首先对传输的信息进行hash运算(这里使用 SHA256),然后用私钥在hash的结果上生成签名。

//对明文进行hash运算
hash := sha256.Sum256(plainText)
fmt.Printf("The hash of my message is: %#x\n", hash)
// The hash of my message is: 0xe6a8502561b8e2328b856b4dbe6a9448d2bf76f02b7820e5d5d4907ed2e6db80

//用私钥在 hash 结果上生成签名
signature, err := rsa.SignPKCS1v15(rand.Reader, privKey, crypto.SHA256, hash[:])
if err != nil {
    log.Fatalf("error creating signature: %v", err)
}

接下来,使用公钥对信息以及该信息对应的签名进行认证,来确认信息没有被伪造被修改。可以看到前2种情况,由于信息和签名都对不上,认证失败。

//用上文生成的公钥对明文和签名进行认证,以确认传输过程中信息没有被修改
verify := func(pub *rsa.PublicKey, msg, signature []byte) error {
    hash := sha256.Sum256(msg)
    return rsa.VerifyPKCS1v15(pub, crypto.SHA256, hash[:], signature)
}

fmt.Println(verify(&privKey.PublicKey, plainText, []byte("a bad signature")))
// crypto/rsa: verification error
fmt.Println(verify(&privKey.PublicKey, []byte("a different plain text"), signature))
// crypto/rsa: verification error
fmt.Println(verify(&privKey.PublicKey, plainText, signature))
// <nil>

因此数字签名可以用于保证我们收到的信息的正确性。

生成自签名证书

crypto/x509 是用来生成数字证书的包。首先,要知道一个证书分为两部分: 公钥+证书持有者信息。

第二部分信息包括序列号,有效期,采用的签名算法等等。我们将这部分信息用通用的函数封装起来,作为一个证书模板。

//证书模板,通过该模板默认设置一些证书需要的字段,比如序列号,组织信息,有效期等等
func CertTemplate() (*x509.Certificate, error) {
    //生成随机的序列号 (不同组织可以有不同的序列号生成方式)
    serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
    serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
    if err != nil {
        return nil, errors.New("failed to generate serial number: " + err.Error())
    }
    tmpl := x509.Certificate{
        SerialNumber:          serialNumber,
        Subject:               pkix.Name{Organization: []string{"Yhat, Inc."}},
        SignatureAlgorithm:    x509.SHA256WithRSA,
        NotBefore:             time.Now(),
        NotAfter:              time.Now().Add(time.Hour), //1小时的有效期
        BasicConstraintsValid: true,
    }
    return &tmpl, nil
}

接下来,我们创建一对新的公私钥 rootKey ,以及一个证书模板 rootCertTmpl

//生成一对新的公私钥
rootKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
    log.Fatalf("generating random key: %v", err)
}
rootCertTmpl, err := CertTemplate()
if err != nil {
    log.Fatalf("creating cert template: %v", err)
}
//在模板的基础上增加一些新的证书信息
rootCertTmpl.IsCA = true   //是否是CA
rootCertTmpl.KeyUsage = x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature
rootCertTmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}
rootCertTmpl.IPAddresses = []net.IP{net.ParseIP("127.0.0.1")}

接下来,万事具备,我们生成一个自签名的证书。一个证书必须由其父证书的私钥签名。当然,根证书是没有父证书的,根证书的私钥由CA机构自己保存。以下生成一个证书的过程:

x509.CreateCertificate 需要 4 个参数:

  • 证书申请者的证书模板
  • 父证书
  • 证书申请者的公钥
  • 父证书对应的公私钥对
func CreateCert(template, parent *x509.Certificate, pub interface{}, parentPriv interface{}) (
    cert *x509.Certificate, certPEM []byte, err error) {

    certDER, err := x509.CreateCertificate(rand.Reader, template, parent, pub, parentPriv)
    if err != nil {
        return
    }
    // parse the resulting certificate so we can use it again
    cert, err = x509.ParseCertificate(certDER)
    if err != nil {
        return
    }
    //将 certDER 用 pem 编码,生成 certPEM 证书
    b := pem.Block{Type: "CERTIFICATE", Bytes: certDER}
    certPEM = pem.EncodeToMemory(&b)
    return
}

那么没有父证书的根证书如何生成呢?根证书的父证书就是自己

rootCert, rootCertPEM, err := CreateCert(rootCertTmpl, rootCertTmpl, &rootKey.PublicKey, rootKey)
if err != nil {
    log.Fatalf("error creating cert: %v", err)
}
fmt.Printf("%s\n", rootCertPEM)
fmt.Printf("%#x\n", rootCert.Signature) // 证书的签名信息

以下是生成的证书信息

-----BEGIN CERTIFICATE-----
MIIDDDCCAfSgAwIBAgIRANeLpYEH+dj0bnlDxJF5sMYwDQYJKoZIhvcNAQELBQAw
FzEVMBMGA1UEChMMY2FzdGVyLCBJbmMuMB4XDTE4MDgwNjEyNDAwMFoXDTE4MDgw
NjEzNDAwMFowFzEVMBMGA1UEChMMY2FzdGVyLCBJbmMuMIIBIjANBgkqhkiG9w0B
AQEFAAOCAQ8AMIIBCgKCAQEAqDEF45u26kMg9tidYJWXbpCZhNwQgBGmYRjOvWtl
7ofQqmhBodUYTdy/fRvKqKOqY2fd00o/qF4AcdRvlVX2KgD2sw3owvd+RVcVoPX1
TtjS4jdPyrdpjwODRvmeGkXHkGC2PVc4eptQZcd9RmZOMnnZG+Up/KQzaJkZOv3G
YVw1K8ipxD3+u2H5INnlGf0LR5WXn3wQoablM/bNG9Plb3xxRVOMsWPMmvYnLXr1
5ElqkyrH2CAi+ECfPEHg8yJAl/IwBPi0DGsVVtF5W5S7/Yw95ym9s2mjWzjK/qWw
8DJVGecjkLpDYSOUF//66xyGooJylzsW8vrrB7GW/SeEpQIDAQABo1MwUTAOBgNV
HQ8BAf8EBAMCAoQwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMA8GA1Ud
EwEB/wQFMAMBAf8wDwYDVR0RBAgwBocEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEA
Qjop8reUUe59TH4RYuR31iQ7iQUmKnq4kRK0kGTe4WLDOVO9ocbc1WyYU8M+so5S
w/ep2tAVQMZ5ofhRf2fgxRKLv1vqNLfYnDn2yBoxQ+r+hdz7dirEr40iTCk3exDT
zYPGZ2b6rUKOdWhDJ2YjuUFm6MX7Z7sNCH0RhCTaCxuEvIH24LzOtSfpoekjuSGF
jPAjmxv/rhB3fqdPT4/7SpB/1hqUkJSZv0QDf6etceG7LWUa7JdNmOhZDrZJNmK4
8c98edPM7uHqN6SolMpaRbQTj/F+9LDbi6gkCN3bTENuxw2Uy3PPNp0FHogyQPQa
4xOrRE3lEuoto5V/ZxLhAg==
-----END CERTIFICATE-----

但是,目前为止的证书实际上只相当于一个公钥,我们还不能把证书放在服务器上。为了证明你是证书的真实拥有者,必须还要有配对的私钥。生成私钥:

// 将私钥用 pem 编码
rootKeyPEM := pem.EncodeToMemory(&pem.Block{
    Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(rootKey),
})
fmt.Printf("rootKeyPEM :\n%s\n", rootKeyPEM)
// 将证书和私钥都 pem 编码之后,结合起来生成最终的 TLS 证书
rootTLSCert, err := tls.X509KeyPair(rootCertPEM, rootKeyPEM)
if err != nil {
    log.Fatalf("invalid key pair: %v", err)
}

私钥生成如下:

-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAuNTbfJcpwmi7zMFJGE3tfYljhVpSXEzmNC5TylGAbHGAO5EC
sM2ymT77n3FvcIMGjPV+4D94cRPF7uanaHpXo+3CT/s6XdFxggVipIZQirmWVhNa
KEP2G4ufFbBeWY+KnFwnfXQpHa/4iibz5263wYl6xfAe9JBgcilwFMtP5b+Sxi6p
lvqWutZsy5kR66HRl9xR/rr2Nf+Az98+Out0HYA4zM5izgL/uhLerLuUosFiFDsR
4VcH49Kvbwsfyv0/Agh+ylq3Y/ssj4aSiGiPOT9QyyF+iJ2uoOSBvXDvjjtmGWiD
NMsQ6lrXyTzEA2boDZ/+R5iurFcviqrYbtsgOwIDAQABAoIBACQsNFBr3RZZHPfz
k/SXu7Tn4HxGsvuxaRQpROjBjpqqk+gUdyxW9W8cbm5D6wVf/zYzDYOhqFapAgHB
Tl4aI3DHpVG13zRhOw+xMh700mpz68Iow2pB8rZtWtMJ000/1GbJekkJJMrUl5Wi
DfXrKzdLSqXWWpiOcPGmvnKzX42c3V3AX4vBEuXxHT7gk/apUU40t8bIYwDMxH/c
xuC0knUhcYA4uEYbyai7oL3ioUI4xh6M5/6vYCurNtBDKNG/U0zLbYAf/UJWFm7a
6aA4NgdDgvGDqONBtsUnZyBnpAQr0xTyhmCAgeLknb9TP5ogtHVbumd5gvDNrNkU
3slJSCECgYEA2s3ixrxl1XwVKMRl1LZtxE8aAyGwct1WSvruSiOOMR4eu+Txp673
nls9ZjdiBl4/Dh0qVwHC03dKRz/shZ/8d6RVIV3kLthZEZ4sa340FRcoVK+APIS+
+d500oX//NDsY3xPVOFOMyWxg7IzF4RP2kRgb6QtjeKMnfIFxvtHwSkCgYEA2ECD
05ot81zu8v4ul3rL3Ts53ZViI61LLOCcq301XAhdUuC2W+XrVIGT2rVkbkaLV8VY
iID70TRhE8QY87pZVoNKLDZsSVjoyUovT1XaxjqmK3ihAqVAByKEhDpM0Cxn+XTy
of2awf8h814dDk9rtc23KLOMIfrCbqbw/yAkzsMCgYAblXUPZNTZswjf2NKVnGH+
K5K17ltWP70POs8rnYvheVCak2Q7pX0mA46cAkNjViJQ3zBlQ52SFynQDaj9t4uW
cashx7pqhW/FHtGuw3xBZGf7NRzPhFSnH3pOyAHbl2MVr6g4pSa8n/XfCmoSfuWq
OJCHwoTTrEnZ55b+3NLQ8QKBgQCmhAb+SRpY6paURWVa/xM7pv9HwF9xWV8pj0sU
QbV0yHwT9TR2TvSGfcB8CHDs+SUS0ML7WVaOIOcfcUBFbJieJTpYERAQ6oVVeeo0
DMgJG+AYWSqh/tzuoYWoy7uaEJd/Xq32TnF8MBjUbQOyoTUvKNiAXsDo6U4OJj4s
NXQiQQKBgDSqI5NOSOZscSHV8P4j7Qo3l9Rzx7DjzrCx0PdhTKUsxxq20SPEixeU
h2OfWCsAziKutJssWg3+saNFlf5IOv3baGxmTV+BtBFyetkz8NrY+QdVJj+UuPWU
Z6vjEVw+Cfu3NTtO4Q+1CJTVye6r5logi512oj/TaiNYRaD+Sgwa
-----END RSA PRIVATE KEY-----

OK,至今为止,TLS 证书准备完毕。下面启动一个带 TLS 认证的http服务器,然后 httptest 模拟发送请求

ok := func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("HI!")) }
s := httptest.NewUnstartedServer(http.HandlerFunc(ok))

//将上述生成的 TLS 证书配置在服务器上
s.TLS = &tls.Config{
    Certificates: []tls.Certificate{rootTLSCert},
}
s.StartTLS()
fmt.Println(s.URL)

//模拟浏览器向服务器发送一个请求
_, err = http.Get(s.URL)
s.Close()

//请求失败,由于服务器返回的证书并不是操作系统或浏览器内置的受信证书
fmt.Println(err)
//Get https://127.0.0.1:47254: x509: certificate signed by unknown authority
//2018/08/07 10:15:49 http: TLS handshake error from 127.0.0.1:47255: remote error: tls: bad certificate

可以看到在握手阶段就失败了,由于服务端的证书不受客户端信任,因此客户端拒绝了连接。

默认的 net/http 会从操作系统加载所有受信任的证书,同时浏览器里也会内置这些受信任的证书。现在的问题是,服务器提供的数字签名证书,对于浏览器或客户端来说,并不在受信任的证书列表中。

模拟CA颁发证书

在解决上述客户端不信任服务器的问题之前,我们首先模拟一个真实场景,某个CA机构给某个组织颁发证书。假设上文中我们生成的 rootCert 是该 CA 的证书。那么接下来使用该证书给第三方组织颁发证书。

首先需要第三方组织自己提供公钥 + 证书模板(主要就是组织的信息等等)

//第三方组织先自己生成一对公私钥
servKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
    log.Fatalf("generating random key: %v", err)
}

//第三方组织提供一个证书模板,包括自己公司的信息,ip 等等
servCertTmpl, err := CertTemplate()
if err != nil {
    log.Fatalf("creating cert template: %v", err)
}
servCertTmpl.KeyUsage = x509.KeyUsageDigitalSignature
servCertTmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}
servCertTmpl.IPAddresses = []net.IP{net.ParseIP("127.0.0.1")}

接下来,由CA机构来给第三方机构签名,颁发证书。注意这一步与上面生成自签名证书一致,只是其父证书变成了我们之前生成的 rootCert

//使用自签名的CA证书给二级组织颁发证书
_, servCertPEM, err := CreateCert(servCertTmpl, rootCert, &servKey.PublicKey, rootKey)
if err != nil {
    log.Fatalf("error creating cert: %v", err)
}

现在我们有了CA颁发的证书,那么如何将证书应用到服务器呢,结合相应的私钥即可。

//先将上面生成的私钥用pem编码
servKeyPEM := pem.EncodeToMemory(&pem.Block{
    Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(servKey),
})
//将CA颁发的证书和本地的私钥结合,生成服务器证书
servTLSCert, err := tls.X509KeyPair(servCertPEM, servKeyPEM)
if err != nil {
    log.Fatalf("invalid key pair: %v", err)
}
//用服务器证书来启动服务器
s = httptest.NewUnstartedServer(http.HandlerFunc(ok))
s.TLS = &tls.Config{
    Certificates: []tls.Certificate{servTLSCert},
}

ok,一个CA颁发证书的简单流程就结束了,第三方组织拿到了CA颁发的证书。当然,此时如果你发送一个请求,会发现依然会被客户端 reject , 因为上文中的 rootCert 只是我们自制的证书,CA 机构也是不存在的。

如何让客户端信任服务器

上面模拟了CA给第三方组织颁发证书的过程。那么如何让客户端相信服务端自制的证书呢?我们需要将我们自签名的CA证书加入到客户端的受信证书池中。

//创建一个受信证书池,并将我们自制的CA的证书加入到证书池中
certPool := x509.NewCertPool()
certPool.AppendCertsFromPEM(rootCertPEM)

//给客户端配置这个证书池
client := &http.Client{
    Transport: &http.Transport{
        TLSClientConfig: &tls.Config{RootCAs: certPool},
    },
}

经过以上配置的客户端,除了会信任操作系统或浏览器内置的证书,还会信任证书池中的证书。也就是我们模拟的CA的证书,有了CA的信任,那么CA颁发给第三方组织的证书也可以信任了。具体操作,就是客户端会同时用内置的证书和 certPool 里的证书来校验数字签名。

接下来启动服务器。发送请求。

s.StartTLS()
resp, err := client.Get(s.URL)
s.Close()
if err != nil {
    log.Fatalf("could not make GET request: %v", err)
}
dump, err := httputil.DumpResponse(resp, true)
if err != nil {
    log.Fatalf("could not dump response: %v", err)
}
fmt.Printf("%s\n", dump)

ok, 成功的看到了服务器的回复

HTTP/1.1 200 OK
Content-Length: 3
Content-Type: text/plain; charset=utf-8
Date: Tue, 07 Aug 2018 03:14:29 GMT

HI!

双向认证:让服务器信任客户端

大多数的web服务器并不关心客户端的身份。换句话说,实际上对客户端的身份认证和鉴权实际上是在应用层做的,比如 session tokens等,而不是在tcp 层。如果要开启CS之间的双向认证应该如何做呢?

开启服务端的认证很简单

//配置服务端对客户端的认证,要求客户端必须携带证书
s = httptest.NewUnstartedServer(http.HandlerFunc(ok))
s.TLS = &tls.Config{
    Certificates: []tls.Certificate{servTLSCert},
    ClientAuth:   tls.RequireAndVerifyClientCert, 
}

s.StartTLS()
_, err = client.Get(s.URL)
s.Close()
fmt.Println(err)
//2018/08/07 11:55:01 http: TLS handshake error from 127.0.0.1:56390: tls: client didn't provide a certificate

可以看到服务器开启双向认证之后,客户端的连接就无法建立了,因为客户端没有提供服务器要求的证书。

接下来是生成一个客户端的证书,跟生成服务端证书类似,先生成公私钥对和证书模板

//创建客户端的公私钥对
clientKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
    log.Fatalf("generating random key: %v", err)
}
//创建客户端证书模板
clientCertTmpl, err := CertTemplate()
if err != nil {
    log.Fatalf("creating cert template: %v", err)
}
clientCertTmpl.KeyUsage = x509.KeyUsageDigitalSignature
clientCertTmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}

同样的,模拟CA机构给客户端颁发证书

//使用 CA 的证书给客户端的公钥+证书信息签名
_, clientCertPEM, err := CreateCert(clientCertTmpl, rootCert, &clientKey.PublicKey, rootKey)
if err != nil {
    log.Fatalf("error creating cert: %v", err)
}
//给客户端的私钥进行 pem 编码
clientKeyPEM := pem.EncodeToMemory(&pem.Block{
    Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(clientKey),
})
//生成客户端 TSL 证书
clientTLSCert, err := tls.X509KeyPair(clientCertPEM, clientKeyPEM)
if err != nil {
    log.Fatalf("invalid key pair: %v", err)
}

客户端证书生成之后,需要配置向服务端提供自己的证书

authedClient := &http.Client{
    Transport: &http.Transport{
        TLSClientConfig: &tls.Config{
            RootCAs:      certPool,
            Certificates: []tls.Certificate{clientTLSCert}, //提供客户端的证书
        },
    },
}

当然,这一切都是在客户端做的,服务端依然不会信任客户端。如果发送请求将会看到:

http: TLS handshake error from 127.0.0.1:59756: tls: failed to verify client's certificate: x509: certificate signed by unknown authority

最后一步,将客户端用到的受信证书池也配置到服务端就可以了:

certPool := x509.NewCertPool()
certPool.AppendCertsFromPEM(rootCertPEM)

s.TLS = &tls.Config{
    Certificates: []tls.Certificate{servTLSCert}, //服务端证书
    ClientAuth:   tls.RequireAndVerifyClientCert,//开启客户端认证模式
    ClientCAs:    certPool,                     //与客户端设置相同的受信证书池
}

这样就可以实现客户端和服务端的双向认证了。

猜你喜欢

转载自blog.csdn.net/u011228889/article/details/81480171
TLS