Go使用TLS实现HTTPS服务器

2021-11-04  本文已影响0人  Go语言由浅入深

本文介绍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),是一个很大的话题,超出了本文的范围。

上一篇下一篇

猜你喜欢

热点阅读