美文网首页
Go使用TLS实现HTTPS服务器

Go使用TLS实现HTTPS服务器

作者: Go语言由浅入深 | 来源:发表于2021-11-04 07:29 被阅读0次

    本文介绍Go使用TLS运行HTTPS服务器和客户端。这里假设你对公钥加密有一定的了解。如果不了解可以查阅RSA和Diffie-Hellman密匙交换。TLS使用Diffie-Hellman的椭圆曲线加密方式。 在这里不详细介绍协议本身是如何工作的,但如果你感兴趣,建议你仔细阅读这方面的资料。
    本文的所有代码都可以在这个仓库中找到。

    简要介绍TLS

    TLS(传输层安全)是一种协议,用于在Internet上实现客户端-服务器安全通信,防止窃听、篡改和消息伪造。在RFC 8446中有描述。

    TLS依赖于最先进的加密技术;这也是为什么建议使用TLS的最新版本,即1.3版本(2021年初)。TLS协议的修订版清除了潜在的不安全的问题,删除了薄弱的加密算法,使协议更安全。

    当客户端使用普通的HTTP协议连接到服务器时,完成标准的TCP握手(SYN -> SYN-ACK -> ACK)之后,客户端就开始发送封装在TCP数据包中的明文数据。使用TLS,情况有点复杂:



    完成TCP握手之后,服务端和客户端执行TLS握手,协商一个他们(以及这个特定会话)独有的公匙。然后使用这个公匙对它们之间交换的所有数据安全地加密。虽然这里有很多事情要做,但这是TLS层实现的。我们只需要正确设置TLS服务器(或客户端);在Go中,HTTP和HTTPS服务器之间的实际差异是最小的。

    TLS证书

    在进入如何使用TLS在Go中设置HTTPS服务器的代码之前,让我们先讨论一下证书。在上面的图中,可以看到服务器将证书作为其第一个ServerHello消息的一部分发送给客户端。在形式上这些证书称为X.509证书,在RFC 5280中有描述。

    公钥加密在TLS中起着重要的作用。证书包含服务器公钥、其身份和受信机构(通常是证书机构)的签名。假设你想和https://bigbank.com网站连接;你怎么知道访问的BigBank服务端是正确的?如果有人监听你的连接,拦截了所有的流量,假装是BigBank服务端与你通信(典型的MITM -中间人攻击)。

    证书处理就解决了中间人攻击情况。当您的客户端底层实现了TLS,在访问https://bigbank.com时,它期望BigBank的证书包含可信机构签署的公匙。证书签名可以形成一棵树(可以由a签名,B签名,C签名,等等),但不管怎样,必须有某个受客户端信任的证书授权机构签名。浏览器有一个内置的预信任CA列表(以及它们的证书)。既然你的连接包含了无法复制的可信证书签名,在这里别人就不能冒充Big Bank服务端。

    在Go中生成自签名证书

    对于本地测试来说,使用自签名证书是非常有用的。自签名证书是为某实体生成包含公钥的证书,但该密钥不是由已知的证书颁发机构签署的,而是由服务端自己签署的。虽然自签名证书还有其他一些合法用途,但我们在这里将重点讨论它们在测试中的用途。

    Go标准库对所有与加密、TLS和证书相关的东西都有很好的支持。让我们看看如何在Go中生成自签名证书:
    第一步生成私钥:

    privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
    if err != nil {
      log.Fatalf("Failed to generate private key: %v", err)
    }
    

    这段代码使用crypto/ecdsa、crypto/elliptic和crypto/rand包生成一个新的密钥对,使用的是P-256椭圆曲线,这是TLS 1.3中允许的曲线之一。
    接下来,创建证书模版:

    serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
    serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
    if err != nil {
      log.Fatalf("Failed to generate serial number: %v", err)
    }
    
    template := x509.Certificate{
      SerialNumber: serialNumber,
      Subject: pkix.Name{
        Organization: []string{"My Corp"},
      },
      DNSNames:  []string{"localhost"},
      NotBefore: time.Now(),
      NotAfter:  time.Now().Add(3 * time.Hour),
    
      KeyUsage:              x509.KeyUsageDigitalSignature,
      ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
      BasicConstraintsValid: true,
    }
    

    每个证书都需要一个唯一的序列号;通常,证书颁发机构会将这些数据存储在某个数据库中,但对于我们的本地测试,使用一个随机的128位数字就可以了。这就是上面代码前几行所做的事情。

    接下来是x509.Certificate模版,有关字段含义的更多信息,请参阅crypto/x509包文档以及RFC 5280。我们只需注意证书在3小时内有效,并且只对localhost域名有效。

    derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
    if err != nil {
      log.Fatalf("Failed to create certificate: %v", err)
    }
    

    证书是从模板创建的,并使用我们前面生成的私钥签名。注意,&template是作为CreateCertificate的模板和参数传入的。后者使得这个证书是自签名的。

    就这样,我们有服务器的私钥和它的证书(其中包含公钥和其他信息)。现在剩下的就是将它们序列化成文件。首先,序列化证书文件:

    pemCert := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
    if pemCert == nil {
      log.Fatal("Failed to encode certificate to PEM")
    }
    if err := os.WriteFile("cert.pem", pemCert, 0644); err != nil {
      log.Fatal(err)
    }
    log.Print("wrote cert.pem\n")
    

    然后,私匙文件:

    privBytes, err := x509.MarshalPKCS8PrivateKey(privateKey)
    if err != nil {
      log.Fatalf("Unable to marshal private key: %v", err)
    }
    pemKey := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privBytes})
    if pemKey == nil {
      log.Fatal("Failed to encode key to PEM")
    }
    if err := os.WriteFile("key.pem", pemKey, 0600); err != nil {
      log.Fatal(err)
    }
    log.Print("wrote key.pem\n")
    

    我们将证书和密钥序列化到PEM文件中,证书格式如下所示:

    -----BEGIN CERTIFICATE-----
    MIIBbjCCARSgAwIBAgIRALBCBgLhD1I/4S0fRZv6yfcwCgYIKoZIzj0EAwIwEjEQ
    MA4GA1UEChMHTXkgQ29ycDAeFw0yMTAzMjcxNDI1NDlaFw0yMTAzMjcxNzI1NDla
    MBIxEDAOBgNVBAoTB015IENvcnAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASf
    wNSifB2LWDeb6xUAWbwnBQ2raSQTqqpaR1C1eEiy6cgqUiiOlr4jUDDiFCly+AS9
    pNNe8o63/Gab/98dwFNQo0swSTAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYI
    KwYBBQUHAwEwDAYDVR0TAQH/BAIwADAUBgNVHREEDTALgglsb2NhbGhvc3QwCgYI
    KoZIzj0EAwIDSAAwRQIgYlJYGIwSvA+AmsHe8P34B5+hlfWEK4+kBmydJ65XJZMC
    IQCzg5aihUXh7Rm0L1K3JrG7eRuTuFSkHoAhzk4cy6FqfQ==
    -----END CERTIFICATE-----
    

    如果您曾经设置过SSH密钥,那么对它的格式应该很熟悉。我们可以使用openssl命令行工具解析它的内容:

    $ openssl x509 -in cert.pem -text
    
    Certificate:
        Data:
            Version: 3 (0x2)
            Serial Number:
                b0:42:06:02:e1:0f:52:3f:e1:2d:1f:45:9b:fa:c9:f7
            Signature Algorithm: ecdsa-with-SHA256
            Issuer: O = My Corp
            Validity
                Not Before: Mar 27 14:25:49 2021 GMT
                Not After : Mar 27 17:25:49 2021 GMT
            Subject: O = My Corp
            Subject Public Key Info:
                Public Key Algorithm: id-ecPublicKey
                    Public-Key: (256 bit)
                    pub:
                        04:9f:c0:d4:a2:7c:1d:8b:58:37:9b:eb:15:00:59:
                        bc:27:05:0d:ab:69:24:13:aa:aa:5a:47:50:b5:78:
                        48:b2:e9:c8:2a:52:28:8e:96:be:23:50:30:e2:14:
                        29:72:f8:04:bd:a4:d3:5e:f2:8e:b7:fc:66:9b:ff:
                        df:1d:c0:53:50
                    ASN1 OID: prime256v1
                    NIST CURVE: P-256
            X509v3 extensions:
                X509v3 Key Usage: critical
                    Digital Signature
                X509v3 Extended Key Usage:
                    TLS Web Server Authentication
                X509v3 Basic Constraints: critical
                    CA:FALSE
                X509v3 Subject Alternative Name:
                    DNS:localhost
        Signature Algorithm: ecdsa-with-SHA256
             30:45:02:20:62:52:58:18:8c:12:bc:0f:80:9a:c1:de:f0:fd:
             f8:07:9f:a1:95:f5:84:2b:8f:a4:06:6c:9d:27:ae:57:25:93:
             02:21:00:b3:83:96:a2:85:45:e1:ed:19:b4:2f:52:b7:26:b1:
             bb:79:1b:93:b8:54:a4:1e:80:21:ce:4e:1c:cb:a1:6a:7d
    

    Go HTTPS服务器

    现在我们有了证书和私钥,就可以运行HTTPS服务器了!尽管安全性是一个非常棘手的问题,但使用标准库非常容易。在将服务器开放到Internet之前,请考虑咨询安全工程师,了解最佳实践以及需要注意哪些配置选项。
    以下是Go实现的一个简单HTTPS服务器:

    func main() {
      addr := flag.String("addr", ":4000", "HTTPS network address")
      certFile := flag.String("certfile", "cert.pem", "certificate PEM file")
      keyFile := flag.String("keyfile", "key.pem", "key PEM file")
      flag.Parse()
    
      mux := http.NewServeMux()
      mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
        if req.URL.Path != "/" {
          http.NotFound(w, req)
          return
        }
        fmt.Fprintf(w, "Proudly served with Go and HTTPS!")
      })
    
      srv := &http.Server{
        Addr:    *addr,
        Handler: mux,
        TLSConfig: &tls.Config{
          MinVersion:               tls.VersionTLS13,
          PreferServerCipherSuites: true,
        },
      }
    
      log.Printf("Starting server on %s", *addr)
      err := srv.ListenAndServeTLS(*certFile, *keyFile)
      log.Fatal(err)
    }
    ```它在根路由上提供了一个处理程序。有趣的部分是TLS配置,以及ListenAndServeTLS调用,获取证书文件和私钥文件的路径(PEM格式,就像我们前面生成的)。TLS配置有许多可能的字段;在这里,我选择了一个相对严格的协议,至少TLS1.3。TLS1.3具有强大开箱即用的安全性,所以如果你能确保你的所有客户端能解析这个版本,是一个不错的选择。
    
    与普通HTTP服务器的区别少于10行代码!服务器的大部分代码(特定路由的处理程序)与底层协议完全无关,不会改变。
    这个服务器在本地运行(并且默认监听在4000端口上),Chrome在访问它时最初会阻止:
    ![](https://img.haomeiwen.com/i21436181/337124108dffb47b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
    这是因为web浏览器在默认情况下不接受自签名证书。如上所述,浏览器自带一个它信任的硬编码CA列表,而我们的自签名证书显然不在其中。我们仍然可以通过点击高级进入服务器,然后允许Chrome继续运行,明确地接受风险。然后它会向我们展示网站(地址栏上有一个红色的“不安全”标志)。
    
    如果我们尝试curl命令访问服务器,也会得到一个错误[4]:
    ```shell
    $ curl -Lv  https://localhost:4000
    
    *   Trying 127.0.0.1:4000...
    * TCP_NODELAY set
    * Connected to localhost (127.0.0.1) port 4000 (#0)
    * ALPN, offering h2
    * ALPN, offering http/1.1
    * successfully set certificate verify locations:
    *   CAfile: /etc/ssl/certs/ca-certificates.crt
      CApath: /etc/ssl/certs
    * TLSv1.3 (OUT), TLS handshake, Client hello (1):
    * TLSv1.3 (IN), TLS handshake, Server hello (2):
    * TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
    * TLSv1.3 (IN), TLS handshake, Certificate (11):
    * TLSv1.3 (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信任我们的服务器。如果我们这样做:

    $ curl -Lv --cacert <path/to/cert.pem>  https://localhost:4000
    
    *   Trying 127.0.0.1:4000...
    * TCP_NODELAY set
    * Connected to localhost (127.0.0.1) port 4000 (#0)
    * ALPN, offering h2
    * ALPN, offering http/1.1
    * successfully set certificate verify locations:
    *   CAfile: /home/eliben/eli/private-code-for-blog/2021/tls/cert.pem
      CApath: /etc/ssl/certs
    * TLSv1.3 (OUT), TLS handshake, Client hello (1):
    * TLSv1.3 (IN), TLS handshake, Server hello (2):
    * TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
    * TLSv1.3 (IN), TLS handshake, Certificate (11):
    * TLSv1.3 (IN), TLS handshake, CERT verify (15):
    * TLSv1.3 (IN), TLS handshake, Finished (20):
    * TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
    * TLSv1.3 (OUT), TLS handshake, Finished (20):
    * SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256
    * ALPN, server accepted to use h2
    * Server certificate:
    *  subject: O=My Corp
    *  start date: Mar 29 13:30:25 2021 GMT
    *  expire date: Mar 29 16:30:25 2021 GMT
    *  subjectAltName: host "localhost" matched cert's "localhost"
    *  issuer: O=My Corp
    *  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 0x557103006e10)
    > GET / HTTP/2
    > Host: localhost:4000
    > user-agent: curl/7.68.0
    > accept: */*
    >
    * TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
    * Connection state changed (MAX_CONCURRENT_STREAMS == 250)!
    < HTTP/2 200
    < content-type: text/plain; charset=utf-8
    < content-length: 33
    < date: Mon, 29 Mar 2021 13:31:34 GMT
    <
    * Connection #0 to host localhost left intact
    Proudly served with Go and HTTPS!
    

    成功!
    我们还可以使用Go编写的自定义HTTPS客户端与服务器通信。代码如下:

    func main() {
      addr := flag.String("addr", "localhost:4000", "HTTPS server address")
      certFile := flag.String("certfile", "cert.pem", "trusted CA certificate")
      flag.Parse()
    
      cert, err := os.ReadFile(*certFile)
      if err != nil {
        log.Fatal(err)
      }
      certPool := x509.NewCertPool()
      if ok := certPool.AppendCertsFromPEM(cert); !ok {
        log.Fatalf("unable to parse cert from %s", *certFile)
      }
    
      client := &http.Client{
        Transport: &http.Transport{
          TLSClientConfig: &tls.Config{
            RootCAs: certPool,
          },
        },
      }
    
      r, err := client.Get("https://" + *addr)
      if err != nil {
        log.Fatal(err)
      }
      defer r.Body.Close()
    
      html, err := io.ReadAll(r.Body)
      if err != nil {
        log.Fatal(err)
      }
      fmt.Printf("%v\n", r.Status)
      fmt.Printf(string(html))
    }
    

    唯一不同于标准HTTP客户端的是TLS设置。重要的是设置TLS的RootCAs字段。配置结构。这告诉Go客户端可以信任哪些证书。

    客户端认证(mTLS)

    到目前为止,我们看到的示例是服务器向客户端提供其(CA签名的)证书,以证明服务器是其所声称的合法身份(例如,银行网站,在您同意提供密码之前需要验证服务端身份)。

    这个想法很容易扩展到相互身份验证,其中客户端也有一个签名的证书来证明其身份。在TLS的世界中,这被称为mTLS(相互TLS),在内部服务必须安全地相互通信中可能非常有用。

    下面是一个简单的带有客户端身份验证的HTTPS服务器:

    func main() {
      addr := flag.String("addr", ":4000", "HTTPS network address")
      certFile := flag.String("certfile", "cert.pem", "certificate PEM file")
      keyFile := flag.String("keyfile", "key.pem", "key PEM file")
      clientCertFile := flag.String("clientcert", "clientcert.pem", "certificate PEM for client authentication")
      flag.Parse()
    
      mux := http.NewServeMux()
      mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
        if req.URL.Path != "/" {
          http.NotFound(w, req)
          return
        }
        fmt.Fprintf(w, "Proudly served with Go and HTTPS!")
      })
    
      // Trusted client certificate.
      clientCert, err := os.ReadFile(*clientCertFile)
      if err != nil {
        log.Fatal(err)
      }
      clientCertPool := x509.NewCertPool()
      clientCertPool.AppendCertsFromPEM(clientCert)
    
      srv := &http.Server{
        Addr:    *addr,
        Handler: mux,
        TLSConfig: &tls.Config{
          MinVersion:               tls.VersionTLS13,
          PreferServerCipherSuites: true,
          ClientCAs:                clientCertPool,
          ClientAuth:               tls.RequireAndVerifyClientCert,
        },
      }
    
      log.Printf("Starting server on %s", *addr)
      err = srv.ListenAndServeTLS(*certFile, *keyFile)
      log.Fatal(err)
    }
    

    这些变化和预期的差不多;除了设置自己的证书、密钥和TLS配置外,服务器还加载客户端证书并将TLSConfig设置为信任它。当然,这也可以是签署客户端证书的本地可信CA的证书。

    下面是一个HTTPS客户端它在连接到服务器时验证自己:

    func main() {
      addr := flag.String("addr", "localhost:4000", "HTTPS server address")
      certFile := flag.String("certfile", "cert.pem", "trusted CA certificate")
      clientCertFile := flag.String("clientcert", "clientcert.pem", "certificate PEM for client")
      clientKeyFile := flag.String("clientkey", "clientkey.pem", "key PEM for client")
      flag.Parse()
    
      // Load our client certificate and key.
      clientCert, err := tls.LoadX509KeyPair(*clientCertFile, *clientKeyFile)
      if err != nil {
        log.Fatal(err)
      }
    
      // Trusted server certificate.
      cert, err := os.ReadFile(*certFile)
      if err != nil {
        log.Fatal(err)
      }
      certPool := x509.NewCertPool()
      if ok := certPool.AppendCertsFromPEM(cert); !ok {
        log.Fatalf("unable to parse cert from %s", *certFile)
      }
    
      client := &http.Client{
        Transport: &http.Transport{
          TLSClientConfig: &tls.Config{
            RootCAs:      certPool,
            Certificates: []tls.Certificate{clientCert},
          },
        },
      }
    
      r, err := client.Get("https://" + *addr)
      if err != nil {
        log.Fatal(err)
      }
      defer r.Body.Close()
    
      html, err := io.ReadAll(r.Body)
      if err != nil {
        log.Fatal(err)
      }
      fmt.Printf("%v\n", r.Status)
      fmt.Printf(string(html))
    }
    

    在测试之前,我们需要更改证书生成脚本,以生成适合客户端的证书。修改以下这一行:

    ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
    ``
    改为:
    

    ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},

    现在我们来试一下。首先为客户端和服务器生成单独的证书/密钥:
    ```shell
    # 客户端证书
    
    $ go run tls-self-signed-cert.go
    2021/04/03 05:51:25 wrote cert.pem
    2021/04/03 05:51:25 wrote key.pem
    $ mv cert.pem clientcert.pem
    $ mv key.pem clientkey.pem
    
    # 服务端证书
    
    $ go run tls-self-signed-cert.go
    2021/04/03 05:51:42 wrote cert.pem
    2021/04/03 05:51:42 wrote key.pem
    

    运行mTLS服务器,它应该根据默认配制参数选择正确的文件:

    $ go run https-server-mtls.go
    2021/11/03 05:54:51 Starting server on :4000
    

    在另一个终端中,如果我们运行旧的(非mtls)客户端,会得到一个错误:

    $ go run https-client.go
    2021/04/03 05:55:24 Get "https://localhost:4000": remote error: tls: bad certificate
    exit status 1
    

    服务器日志显示“客户端没有提供证书”。然而,如果我们运行新的mTLS客户端,工作正常:

    $ go run https-client-mtls.go
    200 OK
    Proudly served with Go and HTTPS!
    

    虽然这演示了运行mTLS服务器和客户端的机制,但实际上还有很多工作要做,特别是管理证书、证书续签和撤销以及受信任的CA。这就是所谓的公钥基础设施(PKI),是一个很大的话题,超出了本文的范围。

    相关文章

      网友评论

          本文标题:Go使用TLS实现HTTPS服务器

          本文链接:https://www.haomeiwen.com/subject/hdlhzltx.html