《互联网协议 — HTTP 超文本传输协议》
《互联网协议 — TLS 1.3 传输层安全协议》
《互联网协议 — TLS — 安全四要素与 CA 认证》
《互联网协议 — HTTPS 安全的超文本传输协议》
Golang 要实现一个 HTTPS 并不困难,只需要使用 http.ListenAndServeTLS 代替 http.ListenAndServe 即可。前者多出了两个形参:certFile 和 keyFile,分别表示本地设备证书和对应的私钥的路径。笔者预先在本地自建了 CA 中心并签发了设备证书,域名为 www.example.com。
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(":443",
"/Users/mickeyfan/workspace/demoCA/server.crt",
"/Users/mickeyfan/workspace/demoCA/server.key", nil)
}
当用户使用浏览器访问 http://www.example.com:443 时,会遭到拒接,错误为:NET::ERR_CERT_INVALID。
可以查看到从 HTTPS 服务器下载到的设备证书的内容:
设备证书包含了设备公钥、数据签名、明文摘要(指纹)等数据。
在建立 TLS 连接的过程中,需要完成两件事情:
数字签名校验用于完成服务端身份认证,设备公钥用于建立非对称加密传输通道,传输的是根据对称加密算法和随机数得到的一串临时对称密钥。这是最常见的混合加密系统。
用户可以强行访问,表示愿意进行非安全连接访问,这时 HTTPS 服务器的设备证书会被保留在操作系统并被信任。
或者手动的将设备证书导入系统,并设置为信任。
这样就可以建立安全访问了。
注意,手动导入证书属于 “人为的确认了证书的身份”,严格的说这并不安全。因为人为很难判断证书是否受过篡改或证书本身就是伪造的。所以,更常见的情况是,服务器会直接将 CA 证书返回给浏览器,并且这个 CA 证书的上层 CA 是权威机构颁发且本地信任的。基于 “证书信任链”,这样才能满足 TLS 安全四要素中的 “身份认证” 要求。
用 curl 指令的话,不使用 CA 证书访问时会被拒绝掉:
$ curl -v -X GET https://www.example.com:443
Note: Unnecessary use of -X or --request, GET is already inferred.
* Trying ::1...
* TCP_NODELAY set
* Connected to www.example.com (::1) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
* CAfile: /etc/ssl/cert.pem
CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (OUT), TLS alert, unknown CA (560):
* SSL certificate problem: unable to get local issuer certificate
* Closing connection 0
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.haxx.se/docs/sslcerts.html
curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.
需要使用 --cacert 选型:
$ curl -v -X GET https://www.example.com:443 --cacert /Users/mickeyfan/workspace/demoCA/ca_01.pem
Note: Unnecessary use of -X or --request, GET is already inferred.
* Trying ::1...
* TCP_NODELAY set
* Connected to www.example.com (::1) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
* CAfile: /Users/mickeyfan/workspace/demoCA/ca_01.pem
CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
* ALPN, server accepted to use h2
* Server certificate:
* subject: C=US; ST=Denial; O=Dis; CN=www.example.com
* start date: Jul 13 06:08:58 2020 GMT
* expire date: Jul 13 06:08:58 2021 GMT
* common name: www.example.com (matched)
* issuer: C=US; ST=Denial; L=Springfield; O=Dis; CN=www.example.com
* SSL certificate verify ok.
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x7fb0b8003800)
> GET / HTTP/2
> Host: www.example.com
> User-Agent: curl/7.64.1
> Accept: */*
>
* Connection state changed (MAX_CONCURRENT_STREAMS == 250)!
< HTTP/2 200
< content-type: text/plain; charset=utf-8
< content-length: 50
< date: Fri, 24 Jul 2020 15:42:49 GMT
<
* Connection #0 to host www.example.com left intact
Hi, This is an example of https service in golang!* Closing connection 0
如果希望忽略证书认证的话使用 -k 选项。
Golang net/http 客户端如果没有没有加载 CA 证书同样会被拒绝掉:
package main
import (
"bytes"
"fmt"
"log"
"net/http"
)
func main() {
resp, err := http.Get("https://www.example.com:443")
if err != nil {
log.Println(err)
return
}
defer resp.Body.Close()
buf := bytes.NewBuffer(make([]byte, 0, 512))
buf.ReadFrom(resp.Body)
fmt.Println(string(buf.Bytes()))
}
结果:
$ go run client.go
2020/07/25 01:16:33 Get "https://www.example.com": x509: certificate signed by unknown authority
服务器也会报错:
2020/07/25 01:20:30 http: TLS handshake error from [::1]:58914: remote error: tls: bad certificate
net/http 加载 CA 证书发起访问:
package main
import (
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"net/http"
)
func main() {
pool := x509.NewCertPool()
caCertPath := "/Users/mickeyfan/workspace/demoCA/ca_01.pem"
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://www.example.com:443")
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 client.go
Hi, This is an example of https service in golang!
当然也可以直接跳过 SSL 认证,使用非安全传输:
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://www.example.com:443")
if err != nil {
fmt.Println("error:", err)
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
fmt.Println(string(body))
}
双向认证,即:服务器认证客户端,客户端也认证服务器。额外增加了服务端对客户端的认证(红色部分)。
package main
import (
"crypto/tls"
"crypto/x509"
"io/ioutil"
"log"
"net/http"
)
func main() {
caCert, err := ioutil.ReadFile("/Users/mickeyfan/workspace/demoCA/ca_01.pem")
if err != nil {
log.Fatal(err)
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
cfg := &tls.Config{
ClientAuth: tls.RequireAndVerifyClientCert, //添加对客户端的认证
//InsecureSkipVerify: true,
ClientCAs: caCertPool,
}
srv := &http.Server{
Addr: ":443",
Handler: &handler{},
TLSConfig: cfg,
}
log.Fatal(srv.ListenAndServeTLS(
"/Users/mickeyfan/workspace/demoCA/client-.pem",
"/Users/mickeyfan/workspace/demoCA/client.key"))
}
type handler struct{}
func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
w.Write([]byte("PONG\n"))
}
package main
import (
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"log"
"net/http"
)
func main() {
caCert, err := ioutil.ReadFile("/Users/mickeyfan/workspace/demoCA/ca_01.pem")
if err != nil {
log.Fatal(err)
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
cert, err := tls.LoadX509KeyPair(
"/Users/mickeyfan/workspace/demoCA/client-.pem",
"/Users/mickeyfan/workspace/demoCA/client.key")
if err != nil {
log.Fatal(err)
}
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: caCertPool,
Certificates: []tls.Certificate{cert},
//InsecureSkipVerify: true,
},
},
}
resp, err := client.Get("https://www.example.com:443")
if err != nil {
log.Println(err)
return
}
htmlData, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
return
}
defer resp.Body.Close()
fmt.Printf("%v\n", resp.Status)
fmt.Printf(string(htmlData))
}
结果:
$ go run client.go
200 OK
PONG
注意:上述示例为了方便,笔者使用了同一套 CA 证书、设备证书和设备密钥。通常,更多的是使用两套不同的密钥。