Go&HTTPS

HTTPS(Hyper Text Transfer Protocol over Secure Socket Layer),是以安全为目标的HTTP通道。HTTPS的安全基础是SSL,因此加密的详细内容就需要SSL,它运行在TCP/IP层之上,应用层之下。

HTTPS工作原理

HTTPS由两部分组成:HTTP+SSL/TLS,也就是在HTTP上又加了一层处理加密信息的模块。工作原理图如下:
https handshake

HTTPS握手详细步骤

  1. 客户端向web服务器发送HTTPS请求(通知可支持的加密算法)
  2. 服务器发送公钥和电子证书,发送是以公钥加密方式发送。电子证书是被认证中心的私钥加密。
  3. 客户端确认电子证书,确认公钥是否为刚才访问的web服务器。如果是则利用被认证中心的公钥解密。
  4. 以对称加密的方式生成密钥(使用web服务器的公钥进行加密),发送到服务器。
  5. 服务器用私钥解密,获得密钥。
  6. 根据对称加密方式的密钥,使用web服务器的公钥进行解密后发送。

HTTPS分类

  1. client端的单向检验
    比如用户在网上进行金钱等操作时,访问的网站就需要提供相应的证书,浏览器会对这些证书进行校验。
  2. server端的单向校验
    例如网银,用户必须提供可以证明自己身份的证书,操作才能继续进行。
  3. server/client的双向校验
    例如微信发红包,它是双向校验。微信平台需要校验用户的身份,用户也需要校验服务器的身份。

HTTPS具体应用

针对HTTPS应用场景的分类,下面对其进行逐一演示。

client端的单向校验

  1. 浏览器的校验
    为了保证CA证书的真实性,浏览器在出厂时都内置了知名CA的相关信息,用来校验服务端发送过来的数字证书。一般服务器证书都会包含诸如站点的名称和主机名、公钥、签发机构(CA)名称和来自签发机构的签名等。对于证书的校验,就是使用浏览器CA证书来验证服务端证书的签名是否是这个CA签发的。
    通过签名我们可以确认:服务端传来的数字证书(数字证书详解)是否是由某个特定CA签发的;其次,服务端传来的数字证书有没有被中途篡改过。一旦签名验证通过,由于我们信任这个CA,从而信任该主机。但是我们也可以自己选择,当服务器提供的证书不是由浏览器CA签发的时候,是否信任该服务器。
package main

import (
    "net/http"
    "log"
)

func handler(w http.ResponseWriter, req *http.Request) {
    w.Header().Set("Content-Type", "text/plain")
    w.Write([]byte("This is an example server\n"))
}

func main() {
    http.HandleFunc("/", handler)
    log.Printf("About to listen on 10443.Go to https://127.0.0.1:10443")
    err := http.ListenAndServeTLS(":10443", "server.crt", "server.key", nil)
    log.Fatal(err)
}

ListenAndServeTLS监听TCP网络地址,然后调用对应的函数处理传入TLS连接上的请求。其中的server.crtserver.key文件是自己用openssl工具生成的,不是通过CA机构颁发的。关于如何用openssl生成这两个文件,在openssl的使用教程(一)中有讲。访问https://localhost:10443或者https://127.0.0.1:10443,浏览器(客户端)会首先提示此站点不安全,然后建议你立即关闭此站点。通过点击详细信息,我们可以看到之所以出现这种情况是因为电脑不信任此网站的安全证书。但是你手动设置,选择继续访问,然后就会出现”this is example server”。
2. Go实现的客户端校验

package main

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

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

执行这个程序,会出现错误:

Get error: Get https://localhost:10443: x509: certificate signed by unknown authority

Go实现的客户端默认对服务端传过来的数字证书进行校验。客户端提示校验错误:这个证书由不知名的CA签发的。那么我们如何才能继续访问这个网站呢?其实方法和浏览器的继续访问一样,只要让client端略过对证书的校验即可。

package main

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

func main() {
    //设置tls配置信息
    tr := &http.Transport{
        TLSClientConfig: &tls.Config{
            InsecureSkipVerify: true,
        },
    }
    client := &http.Client{Transport: tr}
    resp, err := client.Get("https://localhost:10443")
    if err != nil {
        fmt.Println("Get error:", err)
        return
    }
    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    fmt.Println(string(body))
}

Transport是一个对RoundTripper(一个RoundTripper代表一个http事务,给一个请求返回一个响应)的实现,它支持HTTP、HTTPS和HTTP代理。有关Transport的详解可以阅读Go&Transport,其中TLSClientConfigTLS字段是TLS的配置信息,如果为nil,则采用默认配置。
上面的两个例子都是忽略证书的校验,那么如何在不忽略的情况下,访问目的网站呢?根据上文我们知道,之所以客户端会有提示,是因为客户端没有我们自定义的证书机构纳信任的证书机构。所以我们需要给客户端配置一个我们设置的CA,我们首先需要制作以下几个文件。
模拟CA,生成:

私钥文件 ca.key
数字证书 ca.crt

通过模拟的CA,给server颁发证书

私钥文件 server.key
数字证书 server.crt

关于这几个文件生成的具体步骤,可以阅读openssl的使用教程(二)。生成之后,服务端的代码保持不变,只需要将生成的server.keyserver.crt把原来的文件替换掉。客户端的代码则变成下面这样。

package main

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

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)

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

这时执行客户端程序,就可以访问到服务器,而且也不会报错。

双向校验

同样服务端也可以对客户端的证书进行校验,识别客户端的身份,限制客户端的访问。由于Go实现的客户端默认会对服务器进行校验,所以设置server端校验就等于设置双向校验。
同理,验证客户端数字证书,首先需要客户端有自己的证书。制作过程可以阅读openssl的使用教程(二)
server端的程序变成如下:

package main

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

type myhandler struct {
}

func (h *myhandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    w.Header().Set("Content-Type", "text/plain")
    w.Write([]byte("This is an example server\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:    ":10443",
        Handler: &myhandler{},
        TLSConfig: &tls.Config{
            ClientCAs:  pool,
            ClientAuth: tls.RequireAndVerifyClientCert,
        },
    }

    log.Printf("About to listen on 10443.Go to https://127.0.0.1:10443")
    err = s.ListenAndServeTLS("server.crt", "server.key")
    if err != nil {
        log.Fatal(err)
    }
}

客户端的程序变成如下:

package main

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

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)

    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:10443")
    if err != nil {
        fmt.Println("Get error:", err)
        return
    }
    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    fmt.Println(string(body))
}

执行这两个程序,可以正常访问。

参考文章:
1. Go和HTTPS

猜你喜欢

转载自blog.csdn.net/benben_2015/article/details/79859533