Go 语言编程 — net/http — 支持 HTTPS

目录

文章目录

  • 目录
  • 前文列表
  • 单向认证
    • HTTPS 服务端
    • 客户端
      • 浏览器客户端
      • curl 客户端
      • Golang net/http 客户端
  • 双向认证

前文列表

《互联网协议 — HTTP 超文本传输协议》
《互联网协议 — TLS 1.3 传输层安全协议》
《互联网协议 — TLS — 安全四要素与 CA 认证》
《互联网协议 — HTTPS 安全的超文本传输协议》

单向认证

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。

Go 语言编程 — net/http — 支持 HTTPS_第1张图片

可以查看到从 HTTPS 服务器下载到的设备证书的内容:

Go 语言编程 — net/http — 支持 HTTPS_第2张图片

设备证书包含了设备公钥、数据签名、明文摘要(指纹)等数据。

Go 语言编程 — net/http — 支持 HTTPS_第3张图片
Go 语言编程 — net/http — 支持 HTTPS_第4张图片

在建立 TLS 连接的过程中,需要完成两件事情:

  1. 客户端使用 CA 证书解密得到设备公钥、数据签名。
  2. 客户端使用协商的加密算法,根据设备证书的明文信息计算得到一份数据签名,用于与设备证书中的签名进行检验。

Go 语言编程 — net/http — 支持 HTTPS_第5张图片

数字签名校验用于完成服务端身份认证,设备公钥用于建立非对称加密传输通道,传输的是根据对称加密算法和随机数得到的一串临时对称密钥。这是最常见的混合加密系统。

用户可以强行访问,表示愿意进行非安全连接访问,这时 HTTPS 服务器的设备证书会被保留在操作系统并被信任。

Go 语言编程 — net/http — 支持 HTTPS_第6张图片

或者手动的将设备证书导入系统,并设置为信任。

Go 语言编程 — net/http — 支持 HTTPS_第7张图片

这样就可以建立安全访问了。

Go 语言编程 — net/http — 支持 HTTPS_第8张图片

注意,手动导入证书属于 “人为的确认了证书的身份”,严格的说这并不安全。因为人为很难判断证书是否受过篡改或证书本身就是伪造的。所以,更常见的情况是,服务器会直接将 CA 证书返回给浏览器,并且这个 CA 证书的上层 CA 是权威机构颁发且本地信任的。基于 “证书信任链”,这样才能满足 TLS 安全四要素中的 “身份认证” 要求

curl 客户端

用 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 客户端

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))
}

双向认证

双向认证,即:服务器认证客户端,客户端也认证服务器。额外增加了服务端对客户端的认证(红色部分)。

Go 语言编程 — net/http — 支持 HTTPS_第9张图片

  • HTTPS 服务器
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 证书、设备证书和设备密钥。通常,更多的是使用两套不同的密钥。

你可能感兴趣的:(Golang)