HTTPS实际上就是HTTP穿上了SSL/TLS的外套,我们都知道HTTP属于应用层的协议,是离程序员比较近的,而SSL(安全套接字)与TLS(运输层安全)都属于运输层协议。其实SSL与TLS都是上个世纪的产物,最新版本的TLS建立在SSL 3.0协议规范之上。

TLS/SSl

需要先知道TLS/SSL,才能知道如何在http上加了一层tls/ssl,可以从这里了解到。

HTTPS

先声明一些个人容易混淆的概念

  • https中的安全验证是验证证书的可信性,比如单向验证,是客户端端验证服务端发来的证书是不是可靠。双向验证,则包含服务端验证客户端发来证书的可靠性,服务端和客户端都会根据ca来签发自己的证书然后发给对应方,对应方会使用ca数字证书来验证。比如我们在编程的时候,会在客户端或者服务端加载ca证书,就使用用来验证对方发来的证书的可靠性。

  • 对于证书的验证也可以跳过验证,其实就是自己不验证对方的证书。但是有一些服务端是必须要客户端验证证书,然后带着公钥给服务端发送信息,才能获取接口信息,比如harbor的/statistics接口请求。

  • https中登录验证也就是http中的登录认证,这个和tls/ssl没有关系,也就是和上面的证书认证没有什么关系,即使不使用https也可以使用登录认证的,使用了https也可以使用登录认证,比如harbor的/statistics接口请求。

  • 服务端验证的证书中包含一些登录认证的信息,是既可以完成证书的认证,也可以完成登录的认证的,这是两者之间的联系,当然SSL/TLS主要还是完成加密传输的验证,完成安全性保证。

概述

https = http + 加密 + 认证 + 完整性保护
https并非应用层新协议,只是通信接口部分用SSL和TLS协议代替而已
http直接和tcp通信。使用ssl时,http先和ssl通信,再由ssl和tcp通信

HTTPS单向认证

单向认证的过程

从上图可以看出,服务端拥有一对非对称密钥:B_公钥和B_私钥。详细过程如下:

(1)客户端发起HTTPS请求,将SSL协议版本的信息发送给服务端。
(2)服务端去CA机构申请来一份CA证书,在前面提过,证书里面有服务端公钥和签名。将CA证书发送给客户端
(3)客户端读取CA证书的明文信息,采用相同的hash散列函数计算得到信息摘要(hash目的:验证防止内容被修改),然后用操作系统带的CA的公钥去解密签名(因为签名是用CA的私钥加密的),对比证书中的信息摘要。如果一致,则证明证书是可信的,然后取出了服务端公钥
(4)客户端生成一个随机数(密钥F),用刚才等到的服务端B_公钥去加密这个随机数形成密文,发送给服务端。
(5)服务端用自己的B_私钥去解密这个密文,得到了密钥F
(6)服务端和客户端在后续通讯过程中就使用这个密钥F进行通信了。和之前的非对称加密不同,这里开始就是一种对称加密的方式

分析

先来看看数字证书都有哪些内容?

Issuer--证书的发布机构
发布证书的机构,指明证书是哪个公司创建的(并不是指使用证书的公司)。出了问题具体的颁发机构是要负责的

Valid from,Valid to--证书的有效期
证书的使用期限。过了这个期限证书就会作废,不能使用。

Public key--公钥
刚开始的时候介绍过公钥的概念,用于对消息加密的。

Subject--主题
证书是颁发给谁了,一般是个人或公司名称或机构名称或公司网站的网址。

Signature algorithm--签名所使用的算法
数字证书的数字签名所使用的加密算法,根据这个算法可以对指纹解密。指纹加密的结果就是数字签名。

Thumbprint,Thumbprint algorithm--指纹以及指纹算法(一种HASH算法)
指纹和指纹算法会使用证书机构的私钥加密后和证书放在一起。主要用来保证证书的完整性,确保证书没有修改过。使用者在打开证书时根据指纹算法计算证书的hash值,和刚开始的值一样,则表示没有被修改过。

a) 问题回来了,客户端如何检测数字证书是合法的并是所要请求的公司的?

首先应用程序读取证书中的Issuer(发布机构),然后会在操作系统或浏览器内置的受信任的发布机构中去找该机构的证书(为什么操作系统会有受信任机构的证书?先看完这个流程再来回答)。如果找不到就说明证书是水货,证书有问题,程序给错误信息。如果找到了,或用户确认使用该证书。就会拿上级证书的公钥,解密本级证书,得到数字指纹。然后对本级证书的公钥进行数字摘要算法(证书中提供的指纹加密算法)计算结果,与解密得到的指纹对比。如果一样,说明证书没有被修改过。公钥可以放心使用,可以开始握手通信了。

b) 接下来解答操作系统为什么会有证书发布机构的证书?

其实证书发布机构除了给别人发布证书外,自己也有自己的证书。在操作系统安装好时,受信任的证书发布机构的数字证书就已经被微软安装在操作系统中了,根据一些权威安全机构的评估,选取一些信誉很好并且通过一定安全认证的证书发布机构,把这些证书默认安装在操作系统中并设为信任的数字证书。发布机构持有与自己数字证书对应的私钥,会用这个私钥加密所有他发布的证书及指纹整体作为数字签名。

c) 客户端生成随机数并用公钥加密,让服务端用私钥解密来确保对方是否真的持有私钥。但是,黑客也可以发送字符串让服务器用私钥加密,并得到加密后的信息,从而找到规律,导致私钥的安全性下降。如何解决?

服务端并不是真的加密这个字符串,而是把字符串进行hash计算后再进行加密后发送给客户端。客户端收到后再解密这个hash值与原来字符串的hash值对比,从而确定对方是否持有私钥。

d) 在通信的过程中,黑客可以截获加密内容,虽不能理解具体内容,但可以捣乱,修改内容或重复发送该内容,如何解决?

给通信的内容加版本号或随机值,如果接收到版本号或随机值不相同的信息,双方立刻停止通信。若一直捣乱就无法正常通信,因为有人控制了你的路由器,可以针对你。所以一些对于安全性较强的部门来说就不使用公网,而是内部网络,一般不会被破环通信。

总结

最重要的就是RSA加密体制,还有用于验证服务器是否持有私钥的步骤,又能牵涉到HASH算法。验证成功后,通过对消息体的摘要进行HASH加密就能得到RSA签名了(为了保证信息没被篡改),给服务器解密,确认,正常通信。

HTTPS双向认证

双向认证和单向认证原理基本差不多,单向认证客户端需要认证服务端,而在双向认证中增加了服务端对客户端的认证

双向认证详细过程如下:

(1)客户端发起HTTPS请求,将SSL协议版本的信息发送给服务端。
(2)服务端去CA机构申请来一份CA证书,在前面提过,证书里面有服务端公钥和签名。将CA证书发送给客户端
(3)客户端读取CA证书的明文信息,采用相同的hash散列函数计算得到信息摘要(hash目的:验证防止内容被修改),然后用操作系统带的CA的公钥去解密签名(因为签名是用CA的私钥加密的),对比证书中的信息摘要。如果一致,则证明证书是可信的,然后取出了服务端公钥
(4)客户端发送自己的客户端证书给服务端,证书里面有客户端的公钥:C_公钥
(5)客户端发送支持的对称加密方案给服务端,供其选择
(6)服务端选择完加密方案后,用刚才得到的C_公钥去加密选好的加密方案
(7)客户端用自己的C_私钥去解密选好的加密方案,客户端生成一个随机数(密钥F),用刚才等到的服务端B_公钥去加密这个随机数形成密文,发送给服务端。
(8)服务端和客户端在后续通讯过程中就使用这个密钥F进行通信了。和之前的非对称加密不同,这里开始就是一种对称加密的方式

双向认证的客户端证书一般都可以是如openssl生成的自签名证书,包括 client.crt 和 client.key,这两部分内容可以集成在 p12 证书中, p12 证书可以设置打开密码。

http/https登陆认证

登陆认证就是不同于上面的认证方式,不管是http还是https都可以使用下面的登陆认证的方式。

http1.1使用的认证方式有:

Basic认证

http1.0就定义的认证方式

原理:将”用户名:密码”字符串做base64加密,然后作为首部Authorization字段的值传给服务端

缺点:没有加密,安全等级低

使用率:并不常用

Digest认证

http1.1定义的认证,为了弥补basic认证的不足

原理:发送方请求认证->接收方返回质询码->发送方根据质询码计算响应码发给接收方做验证

优点:提供防止密码被窃听的保护机制

缺点:无法保证用户被伪装

使用率:仍达不到web网站安全要求,使用受限

SSL客户端认证

借助客户端的证书完成认证

双因素认证方式:密码+证书。

使用率:web应用常用的认证方式

FormBase认证

web应用程序各自实现的认证方式,比如

  • OAuth对于Http来说,就是放在Authorization header中的不是用户名密码, 而是一个token。
  • Cookie认证机制:用户输入用户名和密码发起请求,服务器认证后给每个Session分配一个唯一的JSESSIONID,并通过Cookie发送给客户端。 当客户端发起新的请求的时候,将在Cookie头中携带这个JSESSIONID。这样服务器能够找到这个客户端对应的Session。默认的,当我们关闭浏览器的时候,客户端cookie会被删除,可以通过修改cookie 的expire time使cookie在一定时间内有效。但是服务器端的session不会被销毁,除非通过invalidate或超时。

使用率:web应用常用的认证方式

应用

HTTP和HTTPS对比

1、HTTP协议运行在TCP之上,所有传输的内容都是明文,HTTPS运行在SSL/TLS之上,SSL/TLS运行在TCP之上,所有传输的内容都经过SSL/TLS协议加密的,所以SSL/TLS主要还是加密,保证安全性。

2、HTTPS协议需要到CA申请证书。

3、HTTP默认使用80端口,HTTPS默认使用443端口。

4、HTTPS用户访问速度较慢、服务端资源压力较大(因为要进行大量的密钥算法计算,消耗CPU、内存)。因此使用HTTPS的话,需要做好足够的优化。

实现http和https共用一个端口原理

1、HTTP与HTTPS都属于应用层协议,所以只要我们在底层协议中进行反向代理,就可以解决这个问题! 因此我们可以选择底层的tcp服务进行代理!

2、https数据流的第一位是十六进制“16”,转换成十进制就是22,通过数据流的第一位置,决定最终反向代理给http\https服务

跳过https的证书验证

重点是设置 InsecureSkipVerify: true

    tr := &http.Transport{
        TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
    }

    rp := &httputil.ReverseProxy{
        Director: func(req *http.Request) {
            req.URL.Scheme = "https"
            req.URL.Host = p.localAddr
            if p.hostHeaderRewrite != "" {
                req.Host = p.hostHeaderRewrite
            }
            for k, v := range p.headers {
                req.Header.Set(k, v)
            }
        },
        Transport: tr,
    }

    p.s = &http.Server{
        Handler: rp,
    }

    go p.s.Serve(listener)

go和https

http server

使用Go创建一个HTTP Server十分Easy,十几行代码就能搞定:

package main

import (
    "fmt"
    "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w,
     "Hi, This is an example of http service in golang!")
}

func main() {
    http.HandleFunc("/", handler)
    http.ListenAndServe(":8080", nil)
}

执行这段代码:

$ go run server.go

打开浏览器,在地址栏输入”http://localhost:8080", 你会看到“ Hi, This is an example of http service in golang!“输出到浏览器窗口。

不过HTTP毕竟是明文的,在这样一个不安全的世界里,随时存在着窃听(sniffer工具可以简单办到)、篡改甚至是冒充等风险,因此对于一些 对安全比较care的站点或服务,它们需要一种安全的HTTP协议,于是就有了HTTPS。

实现一个最简单的HTTPS Web Server

Golang的标准库net/http提供了https server的基本实现,我们修改两行代码就能将上面的HTTP Server改为一个HTTPS Web Server:

package main

import (
    "fmt"
    "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w,
        "Hi, This is an example of https service in golang!")
}

func main() {
    http.HandleFunc("/", handler)
    http.ListenAndServeTLS(":8081", "server.crt",
                           "server.key", nil)
}

我们用http.ListenAndServeTLS替换掉了http.ListenAndServe,就将一个HTTP Server转换为HTTPS Web Server了。不过ListenAndServeTLS 新增了两个参数certFile和keyFile,需要我们传入两个文件路径。为了让这个例子能先Run起来,我们先执行下面命令,利用openssl生成server.crt和server.key文件,供程序使用,原 理后续详述:

$openssl genrsa -out server.key 2048

Generating RSA private key, 2048 bit long modulus
…………….+++
……………+++
e is 65537 (0×10001)

$openssl req -new -x509 -key server.key -out server.crt -days 365

You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
—–
Country Name (2 letter code) [AU]:
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:localhost
Email Address []:

主要上面的localhost需要填写,不然会出错,执行程序:

go run server.go

通过浏览器访问:https://localhost:8081, chrome浏览器会显示如下画面:

说明这个服务端证书不在你的客户端的可信列表中,客户端对服务端的认证没有通过,浏览器将选择权交到了你的手里,是拒绝访问还是忽略。

忽略继续后,才能看到”Hi, This is an example of https service in golang!“这个结果输出在窗口上。

也可以使用curl工具验证这个HTTPS server:

curl -k https://localhost:8081
Hi, This is an example of http service in golang!

注意如果不加-k,curl会报如下错误:

$curl https://localhost:8081
curl: (60) SSL certificate problem: Invalid certificate chain
More details here: http://curl.haxx.se/docs/sslcerts.html

curl performs SSL certificate verification by default, using a "bundle"
 of Certificate Authority (CA) public keys (CA certs). If the default
 bundle file isn't adequate, you can specify an alternate file
 using the –cacert option.
If this HTTPS server uses a certificate signed by a CA represented in
 the bundle, the certificate verification probably failed due to a
 problem with the certificate (it might be expired, or the name might
 not match the domain name in the URL).
If you'd like to turn off curl's verification of the certificate, use
 the -k (or –insecure) option.

服务端私钥与证书

现在我们来说说服务端私钥与证书的生成。

go的http.ListenAndServeTLS需要两个特别参数,一个是服务端的私钥 文件路径,另外一个是服务端的数字证书文件路径。在测试环境,我们没有必要花钱去购买什么证书,利用openssl工具,我们可以自己生成相 关私钥和自签发的数字证书。

openssl genrsa -out server.key 2048 用于生成服务端私钥文件server.key,后面的参数2048单位是bit,是私钥的长度。

openssl生成的私钥中包含了公钥的信息,我们可以根据私钥生成公钥:

$openssl rsa -in server.key -out server.key.public

我们也可以根据私钥直接生成自签发的数字证书:

$openssl req -new -x509 -key server.key -out server.crt -days 365

server.key和server.crt将作为ListenAndServeTLS的两个输入参数。

我们编写一个Go程序来尝试与这个HTTPS server建立连接并通信。

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
)

func main() {
    resp, err := http.Get("https://localhost:8081")
    if err != nil {
        fmt.Println("error:", err)
        return
    }
    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    fmt.Println(string(body))
}

运行这个client,我们得到如下错误:

$go run client1.go
error: Get https://localhost:8081: x509: certificate signed by unknown authority

此时服务端也给出了错误日志提示:

2015/04/30 16:03:31 http: TLS handshake error from 127.0.0.1:62004: remote error: bad certificate

显然从客户端日志来看,go实现的Client端默认也是要对服务端传过来的数字证书进行校验的,但客户端提示:这个证书是由不知名CA签发的!

我们可以修改一下client1.go的代码,让client端略过对证书的校验:

package main

import (
    "crypto/tls"
    "fmt"
    "io/ioutil"
    "net/http"
)

func main() {
    tr := &http.Transport{
        TLSClientConfig:    &tls.Config{InsecureSkipVerify: true},
    }
    client := &http.Client{Transport: tr}
    resp, err := client.Get("https://localhost:8081")

    if err != nil {
        fmt.Println("error:", err)
        return
    }
    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    fmt.Println(string(body))
}

通过设置tls.Config的InsecureSkipVerify为true,client将不再对服务端的证书进行校验。执行后的结果 也证实了这一点:

$go run client2.go
Hi, This is an example of http service in golang!

双向校验

多数时候,我们需要对服务端的证书进行校验,而不是忽略这个校验。大部分产品需要服务端和客户端双向校验。

我们先来看看如何能让client端实现对Server端证书的校验呢?

为了保证CA证书的真实性,浏览器是在出厂时就内置了这些CA证书的,而不是后期通过通信的方式获取的。CA证书就是用来校验由该CA颁发的数字证书的。

那么如何使用CA证书校验Server证书的呢?这就涉及到数字证书到底是什么了!

我们可以通过浏览器中的”https/ssl证书管理”来查看证书的内容,一般服务器证书都会包含诸如站点的名称和主机名、公钥、签发机构 (CA)名称和来自签发机构的签名等。我们重点关注这个来自签发机构的签名,因为对于证书的校验,就是使用客户端CA证书来验证服务端证书的签名是否这个CA签的。

通过签名验证我们可以来确认两件事:

1、服务端传来的数字证书是由某个特定CA签发的(如果是self-signed,也无妨),数字证书中的签名类似于日常生活中的签名,首先 验证这个签名签的是jcy,而不是jby, jay等。
2、服务端传来的数字证书没有被中途篡改过。这类似于"jcy"有无数种写法,这里验证必须是我自己的那种写法,而不是张三、李四写的"jcy"。

一旦签名验证通过,我们因为信任这个CA,从而信任这个服务端证书。由此也可以看出,CA机构的最大资本就是其信用度。

CA在为客户签发数字证书时是这样在证书上签名的:

数字证书由两部分组成:

1、C:证书相关信息(对象名称+过期时间+证书发布者+证书签名算法….)
2、S:证书的数字签名

其中的数字签名是通过公式S = F(Digest©)得到的。

Digest为摘要函数,也就是 md5、sha-1或sha256等单向散列算法,用于将无限输入值转换为一个有限长度的“浓缩”输出值。比如我们常用md5值来验证下载的大文件是否完 整。大文件的内容就是一个无限输入。大文件被放在网站上用于下载时,网站会对大文件做一次md5计算,得出一个128bit的值作为大文件的 摘要一同放在网站上。用户在下载文件后,对下载后的文件再进行一次本地的md5计算,用得出的值与网站上的md5值进行比较,如果一致,则大 文件下载完好,否则下载过程大文件内容有损坏或源文件被篡改。

F为签名函数。CA自己的私钥是唯一标识CA签名的,因此CA用于生成数字证书的签名函数一定要以自己的私钥作为一个输入参数。在RSA加密 系统中,发送端的解密函数就是一个以私钥作 为参数的函数,因此常常被用作签名函数使用。签名算法是与证书一并发送给接收 端的,比如apple的一个服务的证书中关于签名算法的描述是“带 RSA 加密的 SHA-256 ( 1.2.840.113549.1.1.11 )”。因此CA用私钥解密函数作为F,对C的摘要进行运算得到了客户数字证书的签名,好比大学毕业证上的校长签名,所有毕业证都是校长签发的。

接收端接收服务端数字证书后,如何验证数字证书上携带的签名是这个CA的签名呢?接收端会运用下面算法对数字证书的签名进行校验:

F'(S) ?= Digest(C)

接收端进行两个计算,并将计算结果进行比对:

1、首先通过Digest(C),接收端计算出证书内容(除签名之外)的摘要。
2、数字证书携带的签名是CA通过CA密钥加密摘要后的结果,因此接收端通过一个解密函数F'对S进行“解密”。RSA系统中,接收端使用 CA公钥对S进行“解密”,这恰是CA用私钥对S进行“加密”的逆过程。

将上述两个运算的结果进行比较,如果一致,说明签名的确属于该CA,该证书有效,否则要么证书不是该CA的,要么就是中途被人篡改了。

但对于self-signed(自签发)证书来说,接收端并没有你这个self-CA的数字证书,也就是没有CA公钥,也就没有办法对数字证 书的签名进行验证。因此如果要编写一个可以对self-signed证书进行校验的接收端程序的话,首先我们要做的就是建立一个属于自己的 CA,用该CA签发我们的server端证书,并将该CA自身的数字证书随客户端一并发布。

接下来我们来验证一下客户端对服务端数字证书进行验证(gohttps/5-verify-server-cert)!

首先我们来建立我们自己的CA,需要生成一个CA私钥和一个CA的数字证书:

$openssl genrsa -out ca.key 2048
Generating RSA private key, 2048 bit long modulus
……….+++
………………………….+++
e is 65537 (0×10001)

$openssl req -x509 -new -nodes -key ca.key -subj "/CN=tonybai.com" -days 5000 -out ca.crt

接下来,生成server端的私钥,生成数字证书请求,并用我们的ca私钥签发server的数字证书:

openssl genrsa -out server.key 2048
Generating RSA private key, 2048 bit long modulus
….+++
…………………….+++
e is 65537 (0×10001)

$openssl req -new -key server.key -subj "/CN=localhost" -out server.csr

$openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 5000
Signature ok
subject=/CN=localhost
Getting CA Private Key

现在我们的工作目录下有如下一些私钥和证书文件:

CA:
    私钥文件 ca.key
    数字证书 ca.crt     也可以是pem格式,一样的。

Server:
    私钥文件 server.key
    数字证书 server.crt
还可以有一个证书请求文件csr

接下来,我们就来完成我们的程序。

Server端的程序几乎没有变化:

package main

import (
    "fmt"
    "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w,
        "Hi, This is an example of http service in golang!")
}

func main() {
    http.HandleFunc("/", handler)
    http.ListenAndServeTLS(":8081",
        "server.crt", "server.key", nil)
}

client端程序变化较大,由于client端需要验证server端的数字证书,因此client端需要预先加载ca.crt,以用于服务端数字证书的校验:

package main

import (
    "crypto/tls"
    "crypto/x509"
    "fmt"
    "io/ioutil"
    "net/http"
)

func main() {
    //加载ca证书来验证服务端发来的证书
    pool := x509.NewCertPool()
    caCertPath := "ca.crt"

    caCrt, err := ioutil.ReadFile(caCertPath)
    if err != nil {
        fmt.Println("ReadFile err:", err)
        return
    }
    pool.AppendCertsFromPEM(caCrt)

    tr := &http.Transport{
        TLSClientConfig: &tls.Config{RootCAs: pool},
    }
    client := &http.Client{Transport: tr}
    resp, err := client.Get("https://localhost:8081")
    if err != nil {
        fmt.Println("Get error:", err)
        return
    }
    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    fmt.Println(string(body))
}

运行server和client:

$go run server.go

go run client.go
Hi, This is an example of http service in golang!

这边编码使用了NewCertPool来读取对应的ca证书,我们也可以直接使用系统证书,使用函数SystemCertPool,就是默认使用/etc/ssl/certs/这个路径下的证书,还有通过环境变量SSL_CERT_FILE and SSL_CERT_DIR 来指定。

rootCAs, err := x509.SystemCertPool()
if err != nil {
    return nil, err
}

tlsClientConfig := &tls.Config{
    MinVersion: tls.VersionTLS12,
    RootCAs:    rootCAs,
}
if skipVerify {
    tlsClientConfig.InsecureSkipVerify = true
}
transport := &http.Transport{
    TLSClientConfig: tlsClientConfig,
}
client := &http.Client{
    Transport: transport,
    Timeout:   time.Second * 10,
}

对客户端的证书进行校验(双向证书校验)

服务端可以要求对客户端的证书进行校验,以更严格识别客户端的身份,限制客户端的访问。

要对客户端数字证书进行校验,首先客户端需要先有自己的证书。我们以上面的例子为基础,生成客户端的私钥与证书。

$openssl genrsa -out client.key 2048
Generating RSA private key, 2048 bit long modulus
………………..+++
………………..+++
e is 65537 (0×10001)
$openssl req -new -key client.key -subj "/CN=tonybai_cn" -out client.csr
$openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt -days 5000
Signature ok
subject=/CN=tonybai_cn
Getting CA Private Key

接下来我们来改造我们的程序,首先是server端。

首先server端需要要求校验client端的数字证书,并且加载用于校验数字证书的ca.crt,因此我们需要对server进行更加灵活的控制:

package main

import (
    "crypto/tls"
    "crypto/x509"
    "fmt"
    "io/ioutil"
    "net/http"
)

type myhandler struct {
}

func (h *myhandler) ServeHTTP(w http.ResponseWriter,
                   r *http.Request) {
    fmt.Fprintf(w,
        "Hi, This is an example of http service in golang!\n")
}

func main() {
    pool := x509.NewCertPool()
    caCertPath := "ca.crt"

    caCrt, err := ioutil.ReadFile(caCertPath)
    if err != nil {
        fmt.Println("ReadFile err:", err)
        return
    }
    pool.AppendCertsFromPEM(caCrt)

    s := &http.Server{
        Addr:    ":8081",
        Handler: &myhandler{},
        TLSConfig: &tls.Config{
            ClientCAs:  pool,
            ClientAuth: tls.RequireAndVerifyClientCert,
        },
    }

    err = s.ListenAndServeTLS("server.crt", "server.key")
    if err != nil {
        fmt.Println("ListenAndServeTLS err:", err)
    }
}

可以看出代码通过将tls.Config.ClientAuth赋值为tls.RequireAndVerifyClientCert来实现Server强制校验client端证书。ClientCAs是用来校验客户端证书的ca certificate。

Client端变化也很大,需要加载client.key和client.crt用于server端连接时的证书校验:

package main
import (
    "crypto/tls"
    "crypto/x509"
    "fmt"
    "io/ioutil"
    "net/http"
)

func main() {
    //加载ca证书来验证服务端发来的证书
    pool := x509.NewCertPool()
    caCertPath := "ca.crt"

    caCrt, err := ioutil.ReadFile(caCertPath)
    if err != nil {
        fmt.Println("ReadFile err:", err)
        return
    }
    pool.AppendCertsFromPEM(caCrt)

    //发送过程中加上自身的证书给对方验证
    cliCrt, err := tls.LoadX509KeyPair("client.crt", "client.key")
    if err != nil {
        fmt.Println("Loadx509keypair err:", err)
        return
    }

    tr := &http.Transport{
        TLSClientConfig: &tls.Config{
            RootCAs:      pool,
            Certificates: []tls.Certificate{cliCrt},
        },
    }
    client := &http.Client{Transport: tr}
    resp, err := client.Get("https://localhost:8081")
    if err != nil {
        fmt.Println("Get error:", err)
        return
    }
    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    fmt.Println(string(body))
}

好了,让我们来试着运行一下这两个程序,结果如下:

$go run server.go
2015/04/30 22:13:33 http: TLS handshake error from 127.0.0.1:53542:
tls: client's certificate's extended key usage doesn't permit it to be
used for client authentication

$go run client.go
Get error: Get https://localhost:8081: remote error: handshake failure

失败了!从server端的错误日志来看,似乎是client端的client.crt文件不满足某些条件。

根据server端的错误日志,搜索了Golang的源码,发现错误出自crypto/tls/handshake_server.go。

k := false
for _, ku := range certs[0].ExtKeyUsage {
    if ku == x509.ExtKeyUsageClientAuth {
        ok = true
        break
    }
}
if !ok {
    c.sendAlert(alertHandshakeFailure)
    return nil, errors.New("tls: client's certificate's extended key usage doesn't permit it to be used for client authentication")
}

大致判断是证书中的ExtKeyUsage信息应该包含clientAuth。翻看openssl的相关资料,了解到自CA签名的数字证书中包含的都是一些basic的信息,根本没有ExtKeyUsage的信息。我们可以用命令来查看一下当前client.crt的内容:

$ openssl x509 -text -in client.crt -noout
Certificate:
    Data:
        Version: 1 (0×0)
        Serial Number:
            d6:e3:f6:fa:ae:65:ed:df
        Signature Algorithm: sha1WithRSAEncryption
        Issuer: CN=tonybai.com
        Validity
            Not Before: Apr 30 14:11:34 2015 GMT
            Not After : Jan  6 14:11:34 2029 GMT
        Subject: CN=tonybai_cn
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
            RSA Public Key: (2048 bit)
                Modulus (2048 bit):
                    00:e4:12:22:50:75:ae:b2:8a:9e:56:d5:f3:7d:31:
                    7b:aa:75:5d:3f:90:05:4e:ff:ed:9a:0a:2a:75:15:
                    … …
                Exponent: 65537 (0×10001)
    Signature Algorithm: sha1WithRSAEncryption
        76:3b:31:3e:9d:b0:66:ad:c0:03:d4:19:c6:f2:1a:52:91:d6:
        13:31:3a:c5:d5:58:ea:42:1d:b7:33:b8:43:a8:a8:28:91:ac:
         … …

而偏偏golang的tls又要校验ExtKeyUsage,如此我们需要重新生成client.crt,并在生成时指定extKeyUsage。经过摸索,可以用如下方法重新生成client.crt:

1、创建文件client.ext

内容:

extendedKeyUsage=clientAuth

2、重建client.crt

$openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -extfile client.ext -out client.crt -days 5000
Signature ok
subject=/CN=tonybai_cn
Getting CA Private Key

再通过命令查看一下新client.crt:

看到输出的文本中多了这么几行:

        X509v3 extensions:
            X509v3 Extended Key Usage:
                TLS Web Client Authentication

这说明client.crt的extended key usage已经添加成功了。我们再来执行一下server和client:

$ go run client.go
Hi, This is an example of http service in golang!

client端证书验证成功,也就是说双向证书验证均ok了。

通过上面的例子可以看出,使用golang开发https相关程序十分便利,Golang标准库已经实现了TLS 1.2版本协议。