golang 1.15+版本上,需要开启SAN扩展

刚开始写完代码发现一直连接不上去,出现错误X509 Common Name field, use SANs or temporarily enable Common Name matching

造成的原因是因为我用的证书,并没有开启SAN扩展(默认是没有开启SAN扩展)所生成的,所以在导致客户端和服务端无法建立连接, 所以我们要根据提示来解决这个问题

第1步:生成 CA 根证书

openssl genrsa -out ca.key 2048

1
2
3
4
CopyGenerating RSA private key, 2048 bit long modulus (2 primes)
.............+++++
..................................................................................................................+++++
e is 65537 (0x010001)

openssl req -new -x509 -days 3650 -key ca.key -out ca.pem

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CopyYou are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:cn
State or Province Name (full name) [Some-State]:hubei
Locality Name (eg, city) []:wuhan
Organization Name (eg, company) [Internet Widgits Pty Ltd]:hust
Organizational Unit Name (eg, section) []:huster
Common Name (e.g. server FQDN or YOUR name) []:localhost
Email Address []:hust@hust.com

第2步:用 openssl 生成 ca 和双方 SAN 证书。

准备默认 OpenSSL 配置文件于当前目录

使用命令openssl version -a即可查看

1:cp 目录到项目目录进行修改设置

2:找到 [ CA_default ],打开 copy_extensions = copy

3:找到[ req ],打开 req_extensions = v3_req # The extensions to add to a certificate request

4:找到[ v3_req ],添加 subjectAltName = @alt_names

5:添加新的标签 [ alt_names ] , 和标签字段

1
2
3
Copy[ alt_names ]
DNS.1 = localhost
DNS.2 = fengyun.com

这里填入需要加入到 Subject Alternative Names 段落中的域名名称,可以写入多个。

第3步:生成服务端证书

openssl genpkey -algorithm RSA -out server.key

1
2
Copy........................................................................................+++++
.......................................+++++

openssl req -new -nodes -key server.key -out server.csr -days 3650 -subj "/C=cn/OU=custer/O=custer/CN=localhost" -config ./openssl.cnf -extensions v3_req

1
CopyIgnoring -days; not generating a certificate

openssl x509 -req -days 3650 -in server.csr -out server.pem -CA ca.pem -CAkey ca.key -CAcreateserial -extfile ./openssl.cnf -extensions v3_req

1
2
3
CopySignature ok
subject=C = cn, OU = custer, O = custer, CN = localhost
Getting CA Private Key

server.csr是上面生成的证书请求文件。ca.pem/ca.key是CA证书文件和key,用来对server.csr进行签名认证。这两个文件在之前生成的。

第4步:生成客户端证书

openssl genpkey -algorithm RSA -out client.key

1
2
Copy........+++++
...........+++++

openssl req -new -nodes -key client.key -out client.csr -days 3650 -subj "/C=cn/OU=custer/O=custer/CN=localhost" -config ./openssl.cnf -extensions v3_req

1
CopyIgnoring -days; not generating a certificate

openssl x509 -req -days 3650 -in client.csr -out client.pem -CA ca.pem -CAkey ca.key -CAcreateserial -extfile ./openssl.cnf -extensions v3_req

1
2
3
CopySignature ok
subject=C = cn, OU = custer, O = custer, CN = localhost
Getting CA Private Key

现在 Go 1.15 以上版本的 GRPC 通信,这样就完成了使用自签CA、Server、Client证书和双向认证

服务端与客户端

tls认证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ca_b, _ := ioutil.ReadFile("../ssl/ca.pem")
block, _ := pem.Decode(ca_b)
ca, _ := x509.ParseCertificate(block.Bytes)

//添加到证书列表
pool := x509.NewCertPool()
pool.AddCert(ca)

//服务端证书
cert, err := tls.LoadX509KeyPair("../ssl/server.pem", "../ssl/server.key")

config := tls.Config{
//验证客户端证书
ClientAuth: tls.RequireAndVerifyClientCert,
//证书链
Certificates: []tls.Certificate{cert},
//信任证书列表
ClientCAs: pool,
}
config.Rand = rand.Reader
service := "127.0.0.1:22443"
listener, err := tls.Listen("tcp", service, &config)

首先从上面我们创建的服务器私钥和pem文件中得到证书cert,并且生成一个tls.Config对象。这个对象有多个字段可以设置,其中TLS配置项ClientAuth: tls.RequireAndVerifyClientCert表明需要对客户端认证,也就是要完成服务器和客户端的双向认证。
然后用tls.Listen开始监听客户端的连接,accept后得到一个net.Conn,后续处理和普通的TCP程序一样。

客户端tls连接的配置与服务端类似,如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//读取解析三方自签名证书
ca_b, _ := ioutil.ReadFile("../ssl/ca.pem")
block, _ := pem.Decode(ca_b)
ca, _ := x509.ParseCertificate(block.Bytes)

//添加到证书列表
pool := x509.NewCertPool()
pool.AddCert(ca)

//客户端证书
cert, err := tls.LoadX509KeyPair("../ssl/client.pem", "../ssl/client.key")

if err != nil {
fmt.Println(err)
}

config := tls.Config{
//证书链
Certificates: []tls.Certificate{cert},
//信任证书列表
RootCAs: pool,
//否验证服务器的证书链和主机名
InsecureSkipVerify: false,
}
conn, err := tls.Dial("tcp", "localhost:22443", &config)

双向通信

服务端针对于连入的客户端交互主要分为两个线程分别处理用户在控制台的输入信息客户端的发送的消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
for {
//log.Print("server: conn: waiting")
n, err := conn.Read(buf)//读取客户端的消息并且打印到控制台上
if err != nil {
if err != nil {
log.Printf("server: conn: read: %s", err)
}
break
}
log.Printf("[client]%v: %q\n (%d bytes)", conn.RemoteAddr(), string(buf[:n]), n)

go func() {//处理控制台输入,将消息发送给客户端
reader := bufio.NewReader(os.Stdin)
bytes, _, _ := reader.ReadLine()
n, err = io.WriteString(conn, string(bytes))
if err != nil {
log.Fatalf("server: %s", err)
}
log.Printf("[server]%v: %q (%d bytes)", conn.LocalAddr(), string(bytes), n)
}()
}

客户端的处理类似,这里就不贴代码浪费空间了,完整代码见附件

交互情况

image-20230107183624310

当对方发送了一条消息或者自己发送了一条消息,控制台上都会动态更新消息。可以实现点对点基本交互。

聊天记录存储

日志

log包可以通过SetOutput()方法指定日志输出的方式(Writer),但是只能指定一个输出的方式(Writer)。我们利用io.MultiWriter()将多个Writer拼成一个Writer使用的特性,把log.Println()输出的内容分流到控制台和文件当中。

1
2
3
4
5
6
7
logFile, err := os.OpenFile("log.txt", os.O_CREATE | os.O_APPEND | os.O_RDWR, 0666)
if err != nil {
panic(err)
}
defer logFile.Close()
mw := io.MultiWriter(os.Stdout,logFile)
log.SetOutput(mw)

这样就可以同时在控制台和日志都存储聊条记录了

客户端输出readlog指令就可以读取log,如下所示

image-20230107184100319

client.log文件如下所示

image-20230107184139462

tls单向认证分析

(1) client_hello

客户端发起请求,以明文传输请求信息,包含版本信息,加密套件候选列表,压缩算法候选列表,随机数,扩展字段等信息,相关信息如下:

  • 支持的最高 TLS 协议版本 version,从低到高依次 SSLv2, SSLv3, TLSv1, TLSv1.1, TLSv1.2, 当前基本不再使用低于 TLSv1 的版本
  • 客户端支持的加密套件 cipher suites 列表, 每个加密套件对应前面 TLS 原理中的四个功能的组合:
    • 认证算法 Au (身份验证)
    • 密钥交换算法 KeyExchange (密钥协商)
    • 对称加密算法 Enc (信息加密)
    • 信息摘要 Mac (完整性校验)
  • 支持的压缩算法 compression methods 列表,用于后续的信息压缩传输
  • 随机数 random_C,用于后续的密钥的生成
  • 扩展字段 extensions,支持协议与算法的相关参数以及其它辅助信息等,常见的 SNI 就属于扩展字段,后续单独讨论该字段作用

(2) server_hello + server_certificate + sever_hello_done

  • server_hello, 服务端返回协商的信息结果,包括选择使用的协议版本 version,选择的加密套件 cipher suite,选择的压缩算法 compression method、随机数 random_S 等,其中随机数用于后续的密钥协商
  • server_certificates, 服务器端配置对应的证书链,用于身份验证与密钥交换
  • server_hello_done,通知客户端 server_hello 信息发送结束

(3) 证书校验

  • 证书/证书链的可信性 trusted certificate path,方法如前文所述
  • 证书是否吊销 revocation,有两类方式离线 CRL 与在线 OCSP,不同客户端行为会不同
  • 有效期 expiry date,证书是否在有效时间范围
  • 域名 domain,核查证书域名是否与当前的访问域名匹配 (CN 字段)

证书校验没有强制的过程,也就是校验严格和校验宽松通常都是可以配置的,由校验端来确定。

(4) client_key_exchange + change_cipher_spec + encrypted_handshake_message

  • client_key_exchange: 合法性验证通过之后,客户端计算产生随机数字 pre-master,并用证书公钥加密,发送给服务器
  • 此时客户端已经获取全部的计算协商密钥需要的信息:两个明文随机数 random_C 和 random_S 与自己计算产生的 pre-master,计算得到协商密钥
    enc_key=Fuc(random_C, random_S, pre-master)
  • change_cipher_spec: 客户端通知服务器后续的通信都采用协商的通信密钥和加密算法进行加密通信;
  • encrypted_handshake_message: 结合之前所有通信参数的 hash 值与其它相关信息生成一段数据,采用协商密钥 session secret 与算法进行加密,然后发送给服务器用于数据与握手验证

(5) change_cipher_spec + encrypted_handshake_message

  • 服务器用私钥解密加密的 pre-master 数据,基于之前交换的两个明文随机数 random_C 和 random_S,计算得到协商密钥:enc_key=Fuc(random_C, random_S, pre-master);
  • 计算之前所有接收信息的 hash 值,然后解密客户端发送的 encrypted_handshake_message,验证数据和密钥正确性;
  • change_cipher_spec, 验证通过之后,服务器同样发送 change_cipher_spec 以告知客户端后续的通信都采用协商的密钥与算法进行加密通信;
  • encrypted_handshake_message, 服务器也结合所有当前的通信参数信息生成一段数据并采用协商密钥 session secret 与算法加密并发送到客户端;

(6) 握手结束

客户端计算所有接收信息的 hash 值,并采用协商密钥解密 encrypted_handshake_message,验证服务器发送的数据和密钥,验证通过则握手完成

(7) 加密通信

开始使用协商密钥与算法进行加密通信。

tls双向认证分析

通过wireshark抓取服务端与客户端传输的包

可以看到实际而言客户端和服务双向tls加密的过程和单向tls加密的过程其实非常类似,和单向认证几乎一样,只是在 client 认证完服务器证书后,client 会将自己的证书 client.crt 传给服务器。服务器验证通过后,开始秘钥协商。

image-20230107184514651

第一包 (No. 51432) Client Hello 包,即 SSL/TLS 单向认证流程的 (1)
第二包 (No. 51434) Server Hello 包,包含服务器证书等。即 SSL/TLS 单向认证流程的 (2)
第三包 (No. 51436) 服务器证书验证完成,同时发送客户端的证书 client.crt ,同时包含 client key exchange+change cipher spec + encrypted handshake message. 即 SSL/TLS 单向认证流程的 (4)
第四包 (No. 51422)服务器验证客户端证书的合法性。通过后进行秘钥协商,change cipher spec + encrypted hanshake message.即 SSL/TLS 单向认证流程的 (5)

总结

每个节点(不管是客户端还是服务端)都有一个证书文件和key文件,他们用来互相加密解密;因为证书里面包含public key,key文件里面包含private key;他们构成一对密钥对,是互为加解密的。

根证书是所有节点公用的,不管是客户端还是服务端,都要先注册根证书(通常这个过程是注册到操作系统信任的根证书数据库里面,在咱们这个例子里面没有这么做,因为这是一个临时的根证书,只在服务端和客户端命令行中指定了一下),以示这个根证书是可信的, 然后当需要验证对方的证书时,因为待验证的证书是通过这个根证书签名的,我们信任根证书,所以推导出也可以信任对方的证书。

所以如果需要实现双向认证,那么每一端都需要三个文件

  • {node}.cer: PEM certificate
    己方证书文件,将会被发给对方,让对方认证
  • {node}..key: PEM RSA private key
    己方private key文件,用来解密经己方证书(因为包含己方public key)加密的内容,这个加密过程一般是由对方实施的。
  • ca.cer: PEM certificate
    根证书文件,用来验证对方发过来的证书文件,所有由同一个根证书签名的证书都应该能验证通过。

问题

1.golang 1.15+版本上,需要开启SAN扩展:刚开始写完代码发现一直连接不上去,出现错误X509 Common Name field, use SANs or temporarily enable Common Name matching,解决方法见前文

2.不是很理解什么叫做聊天记录本地加密存储,一般而言聊天加密通常做到的都是在网络传输过程中能够保证安全性,聊天记录是由用户产生的,难度针对于用户还要保持不可见性?但是针对于聊天记录日志修改了权限,仅限管理员身份才可以打开修改