Golang TLS 编程

目录

基本概念

TLS编程涉及到一些概念,比如非对称加密、私钥、公钥、CA等,需要先理清这些概念,才能把流程打通,说句题外话,之前工作的时候这部分概念理清过,也实现了TLS编程,但是文档写在公司内网了,后来也忘记了,到现在,又花了一些时间才搞清楚。

首先是非对称加密,涉及到公钥和私钥,公钥是随意公布的,公钥加密的内容只有私钥才能解密,私钥加密的东西只有公钥才能解密,一般私钥保存在服务端,是严格保密的,客户端使用公钥加密内容后,发送给服务端,然后服务端使用私钥进行解密。

CA(Certificate Authority),是一些权威机构,比如GoDaddy等,用来验证服务端证书是正确的,没有被篡改的。一个应用服务器想要提供https服务,需要首先产生私钥和CSR(Certificate Signing Request),通过CSR向CA权威机构请求签发证书,这样这个CA就能鉴证服务端证书的真伪。CSR包含这个应用服务端的基本信息,包括:CN(Common name,名字),O(Organization)等;具体可以参考 What is contained in a CSR?

还有个概念自签发证书,就是签发证书不是权威机构做的,而是我们自己生成的 CA 签发的(其实只要 openssl 生成一个私钥,使用这个私钥就可以做正确的签发工作),同时,做签发证书用的的私钥(ca.key)对应的公钥(ca.crt),一般要分发到各种设备中,比如客户端所在的主机或者浏览器中,客户端就可以用这个ca.crt来验证服务端的证书。

在TLS编程中,服务端需要自己的私钥server.key,以及用CSR请求的证书server.crt,用这两个就够了。客户端需要ca.crt用来验证server.crt的有效性,用这个就够了,客户端在编程时可以提供ca.crt,如果不提供,那么这个ca.crt需要实现被导入到主机中,或者被导入到浏览器中。

K8s文档Certificates介绍了整个流程:

a. 生成2048bit的私钥,自签名CA的私钥。

openssl genrsa -out ca.key 2048

b. 生成私钥对应的证书ca.crt,自签名CA的证书,也就是Root Certificate,这个证书需要分发到各个客户端设备中,或者在编写客户端代码的时候直接提供。

openssl req -x509 -new -nodes -key ca.key -subj "/CN=${MASTER_IP}" -days 10000 -out ca.crt

c. 生成应用程序服务端私钥,server.key

openssl genrsa -out server.key 2048

d. 配置CSR配置文件,这个CSR文件需要提供给CA,用来签发证书,假设该配置文件名字为:csr.conf

[ req ]
default_bits = 2048
prompt = no
default_md = sha256
req_extensions = req_ext
distinguished_name = dn

[ dn ]
C = <country>
ST = <state>
L = <city>
O = <organization>
OU = <organization unit>
CN = <MASTER_IP>

[ req_ext ]
subjectAltName = @alt_names

[ alt_names ]
DNS.1 = kubernetes
DNS.2 = kubernetes.default
DNS.3 = kubernetes.default.svc
DNS.4 = kubernetes.default.svc.cluster
DNS.5 = kubernetes.default.svc.cluster.local
IP.1 = <MASTER_IP>
IP.2 = <MASTER_CLUSTER_IP>

[ v3_ext ]
authorityKeyIdentifier=keyid,issuer:always
basicConstraints=CA:FALSE
keyUsage=keyEncipherment,dataEncipherment
extendedKeyUsage=serverAuth,clientAuth
subjectAltName=@alt_names

这里主要关注alt_names字段,这个是服务端程序所运行的主机域名或者IP。签发的证书只能这个IP用。

我们也可以不使用配置文件,直接使用下面命令生成csr,会通过交互的方式生成csr。openssl req -new -key server.key -out server.csr

e. 根据配置文件,生成 CSR 证书,(输入:私钥、csr 配置;输出:csr证书)

openssl req -new -key server.key -out server.csr -config csr.conf

f. 使用ca.key和ca.crt以及csr签发证书,生成的server.crt文件,就是server端启动https server时需要的。(输入:csr证书、ca私钥、ca证书;输出:server 证书)

openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \
-CAcreateserial -out server.crt -days 10000 \
-extensions v3_ext -extfile csr.conf

g. 查看证书

openssl x509  -noout -text -in ./server.crt

上面提到了ca.crt证书的分发,在MacOS系统中,加入到系统的命令为:

sudo security add-trusted-cert -d -r trustRoot -k "/Library/Keychains/System.keychain" ca.crt

使用 cfssl 生成证书

jimmysong.io 创建 TLS 证书和秘钥》介绍了使用它 cfssl 生成证书的方式,在文章介绍的方式中,生成 ca 私钥和证书之后,使用 csr 配置就可以生成一个服务器的证书和私钥,比如在生成 admin 证书时,假设有如下 admin-csr.json 配置。

{
  "CN": "admin",
  "hosts": [],
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "CN",
      "ST": "BeiJing",
      "L": "BeiJing",
      "O": "system:masters",
      "OU": "System"
    }
  ]
}

假设已经有证书文件:ca.pemca-key.pemca-config.json(ca 配置) 生成的 admin-key.pemadmin.pem 证书如下。

$ cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=kubernetes admin-csr.json | cfssljson -bare admin
$ ls admin*
admin.csr  admin-csr.json  admin-key.pem  admin.pem

kube-apiserver 预定义了一些 RBAC 使用的 RoleBindings,如 cluster-admin RoleBinding 将 Group system:masters 与 Role cluster-admin 绑定,该 Role 授予了调用 kube-apiserver 的所有 API 的权限;

关于CSR中的 CN 和 O。CN:Common Name,kube-apiserver 从证书中提取该字段作为请求的用户名 (User Name);浏览器使用该字段验证网站是否合法;O:Organization,kube-apiserver 从证书中提取该字段作为请求用户所属的组 (Group);

Golang TLS编程

现在证书都有了,现在开始TLS编程。另外需要说明一下,我在mac上做实验时,生成证书的方式是参考How to Create Your Own SSL Certificate Authority for Local HTTPS Development,k8s的文档行不通。即步骤如下:

# 生成ca密钥
openssl genrsa -des3 -out myCA.key 2048
# 生成ca证书,这个就是根证书
openssl req -x509 -new -nodes -key myCA.key -sha256 -days 1825 -out myCA.pem
# 将根证书安装到mac系统中
sudo security add-trusted-cert -d -r trustRoot -k "/Library/Keychains/System.keychain" myCA.pem
# 生成服务端密钥
openssl genrsa -out dev.deliciousbrains.com.key 2048
# 生成服务端csr 
openssl req -new -key dev.deliciousbrains.com.key -out dev.deliciousbrains.com.csr
# 用ca签发证书
openssl x509 -req -in dev.deliciousbrains.com.csr -CA myCA.pem -CAkey myCA.key -CAcreateserial \
-out dev.deliciousbrains.com.crt -days 825 -sha256 -extfile dev.deliciousbrains.com.ext

dev.deliciousbrains.com.ext配置文件如下

authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names

[alt_names]
#DNS.1 = dev.deliciousbrains.com
DNS.1 = localhost

server端

http.Server结构中配置TLSConfig之后,就不需要在ListenAndServeTLS中,指定密钥和证书了。

package main

import (
	"crypto/tls"
	"fmt"
	"log"
	"net/http"
)

const (
	cert = "/Users/decent/tls_test/dev.deliciousbrains.com.crt"
	key = "/Users/decent/tls_test/dev.deliciousbrains.com.key"
)

func main()  {
	// generate a "Certificate" struct
	cert, _ := tls.LoadX509KeyPair(cert, key)

	// create a custom server with "TLSConfig"
	s := &http.Server{
		Addr: "localhost:9000",
		Handler: nil,
		TLSConfig: &tls.Config{
			Certificates: []tls.Certificate{cert},
		},
	}

	// handle "/" route
	http.HandleFunc("/", func(res http.ResponseWriter, request *http.Request) {
		fmt.Fprint(res, "hello custom world!")
	})

	// run server on port "9000"
	log.Fatal(s.ListenAndServeTLS("", ""))
}

client端

在client端,我们可以选择不验证服务端的证书,这个时候需要指定InsecureSkipVerify: true,或者我们提供自签名的证书,来验证服务端证书的有效性

package main

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

func main() {
	tr := &http.Transport{
        // 可以不验证服务端证书
		//TLSClientConfig:    &tls.Config{InsecureSkipVerify: true},
        // 使用自签名CA认证
        TLSClientConfig:    &tls.Config{
			RootCAs: loadCA("/Users/decent/tls_test/myCA.pem"),
		},
	}
	client := &http.Client{Transport: tr}
	resp, err := client.Get("https://localhost:9000/")

	if err != nil {
		fmt.Println("error:", err)
		return
	}
	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	fmt.Println(string(body))
}

func loadCA(caFile string) *x509.CertPool {
	pool := x509.NewCertPool()

	if ca, e := ioutil.ReadFile(caFile); e != nil {
		log.Fatal("ReadFile: ", e)
	} else {
		pool.AppendCertsFromPEM(ca)
	}
	return pool
}

上面只是对服务端证书进行认证,如果也需要验证客户端,只与可信任的客户端通信,则也需要对客户端进行通信,方法类似,需要客户端提供证书,并在服务端配置tls.RequireAndVerifyClientCert, 可以参考:verify certificate in double direction

客户端配置:

func createClientConfig(ca, crt, key string) (*tls.Config, error) {
	caCertPEM, err := ioutil.ReadFile(ca)
	if err != nil {
		return nil, err
	}

	roots := x509.NewCertPool()
	ok := roots.AppendCertsFromPEM(caCertPEM)
	if !ok {
		panic("failed to parse root certificate")
	}

	cert, err := tls.LoadX509KeyPair(crt, key)
	if err != nil {
		return nil, err
	}
	return &tls.Config{
		Certificates: []tls.Certificate{cert},
		RootCAs:      roots,
	}, nil
}

服务端配置:

func createServerConfig(ca, crt, key string) (*tls.Config, error) {
	caCertPEM, err := ioutil.ReadFile(ca)
	if err != nil {
		return nil, err
	}

	roots := x509.NewCertPool()
	ok := roots.AppendCertsFromPEM(caCertPEM)
	if !ok {
		panic("failed to parse root certificate")
	}

	cert, err := tls.LoadX509KeyPair(crt, key)
	if err != nil {
		return nil, err
	}
	return &tls.Config{
		Certificates: []tls.Certificate{cert},
		ClientAuth:   tls.RequireAndVerifyClientCert,
		ClientCAs:    roots,
	}, nil
}

关于K8s对于客户端的认证,可以参考:Kubernetes 中的用户与身份认证授权

参考

jimmysong.io 创建 TLS 证书和秘钥

根据kubelet.kubeconfig文件解析到证书

How to Create Your Own SSL Certificate Authority for Local HTTPS Development

What is a CSR (Certificate Signing Request)?

Secure HTTPS servers in Go

A brief overview of the TCP/IP model, SSL/TLS/HTTPS protocols and SSL certificates