Go/TLS

传输层安全协议(英语: Transport Layer Security, 缩写: TLS), 及其前身安全套接层(Secure Sockets Layer, 缩写: SSL)是一种安全协议, 目的是为互联网通信, 提供安全及数据完整性保障.

SSL 包含记录层(Record Layer)和传输层, 记录层协议确定传输层数据的封装格式. 传输层安全协议使用X.509认证, 之后利用非对称加密演算来对通信方做身份认证, 之后交换对称密钥作为会谈密钥(Session key). 这个会谈密钥是用来将通信两方交换的数据做加密, 保证两个应用间通信的保密性和可靠性, 使客户与服务器应用之间的通信不被攻击者窃听.

生成 TLS 私钥与公钥

# 生成 CA 私钥
openssl genrsa -out ca.key 2048
# 生成 CA 证书
openssl req -x509 -new -key ca.key -days 36525 -out ca.crt

# 生成服务端私钥
openssl genrsa -out server.key 2048
# 生成服务端签名请求
openssl req -new -key server.key -subj "/CN=host" -out server.csr
# 使用 CA 对服务端签名请求进行签名, 并生成服务端证书
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 36525

# 生成客户端私钥
openssl genrsa -out client.key 2048
# 生成客户端签名请求
openssl req -new -key client.key -out client.csr
# 使用 CA 对客户端签名请求进行签名, 并生成客户端证书
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt -days 36525

服务端验证

// server
package main

import (
    "bufio"
    "crypto/tls"
    "log"
    "net"
)

func serv(conn net.Conn) {
    defer conn.Close()
    r := bufio.NewReader(conn)
    for {
        line, err := r.ReadString('\n')
        if err != nil {
            log.Println(err)
            break
        }
        log.Println("Receive:", line[:len(line)-1])
    }
}

func main() {
    cert, err := tls.LoadX509KeyPair("server.crt", "server.key")
    if err != nil {
        log.Fatalln(err)
    }

    conf := &tls.Config{Certificates: []tls.Certificate{cert}}
    ln, err := tls.Listen("tcp", ":8080", conf)
    if err != nil {
        log.Fatalln(err)
    }
    defer ln.Close()
    log.Println("Listen and server on :8080")

    for {
        conn, err := ln.Accept()
        if err != nil {
            log.Println(err)
            continue
        }
        go serv(conn)
    }
}
// client
package main

import (
    "crypto/tls"
    "fmt"
    "log"
    "time"
)

func main() {
    conf := &tls.Config{
        InsecureSkipVerify: true,
    }
    conn, err := tls.Dial("tcp", "localhost:8080", conf)
    if err != nil {
        log.Fatalln(err)
    }
    defer conn.Close()

    for _ = range time.NewTicker(time.Second).C {
        conn.Write([]byte(fmt.Sprintf("%d", time.Now().UnixNano())))
        conn.Write([]byte("\n"))
    }
}

注意客户端配置 InsecureSkipVerify 用来控制客户端是否校验证书安全性. 默认情况下, 客户端会检查服务端证书域名与真实请求域名是否一致. 设置为 true 则跳过检查. 这可能导致中间人攻击, 因此该设置仅应在测试过程中置为 true.

服务端验证-安全模式

不配置 InsecureSkipVerify, 客户端必须信任 CA 证书(或服务端证书):

// client
package main

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

func main() {
    caCert, err := ioutil.ReadFile("ca.crt")
    if err != nil {
        log.Fatalln(err)
    }
    caCertPool := x509.NewCertPool()
    caCertPool.AppendCertsFromPEM(caCert)

    conf := &tls.Config{
        RootCAs: caCertPool,
    }
    conn, err := tls.Dial("tcp", "localhost:8080", conf)
    if err != nil {
        log.Fatalln(err)
    }
    defer conn.Close()

    for _ = range time.NewTicker(time.Second).C {
        conn.Write([]byte(fmt.Sprintf("%d", time.Now().UnixNano())))
        conn.Write([]byte("\n"))
    }
}

客户端验证

有时候, 服务端需要验证客户端身份, 以保证不是每个拿到服务端公钥的客户端都可以连接(服务端公钥在传播过程中泄露是难免的). 此时, 服务端需要验证客户端公钥.

// server
package main

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

func serv(conn net.Conn) {
    defer conn.Close()
    r := bufio.NewReader(conn)
    for {
        line, err := r.ReadString('\n')
        if err != nil {
            log.Println(err)
            break
        }
        log.Println("Receive:", line[:len(line)-1])
    }
}

func main() {
    cert, err := tls.LoadX509KeyPair("server.crt", "server.key")
    if err != nil {
        log.Fatalln(err)
    }
    certBytes, err := ioutil.ReadFile("ca.crt")
    if err != nil {
        log.Fatalln(err)
    }
    clientCertPool := x509.NewCertPool()
    clientCertPool.AppendCertsFromPEM(certBytes)
    conf := &tls.Config{
        Certificates: []tls.Certificate{cert},
        ClientAuth:   tls.RequireAndVerifyClientCert,
        ClientCAs:    clientCertPool,
    }
    ln, err := tls.Listen("tcp", ":8080", conf)
    if err != nil {
        log.Fatalln(err)
    }
    defer ln.Close()
    log.Println("Listen and server on :8080")

    for {
        conn, err := ln.Accept()
        if err != nil {
            log.Println(err)
            continue
        }
        go serv(conn)
    }
}
// client
package main

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

func main() {
    cert, err := tls.LoadX509KeyPair("client.crt", "client.key")
    if err != nil {
        log.Fatalln(err)
    }
    caCert, err := ioutil.ReadFile("ca.crt")
    if err != nil {
        log.Fatalln(err)
    }
    caCertPool := x509.NewCertPool()
    caCertPool.AppendCertsFromPEM(caCert)
    conf := &tls.Config{
        Certificates: []tls.Certificate{cert},
        RootCAs:      caCertPool,
    }
    conn, err := tls.Dial("tcp", "localhost:8080", conf)
    if err != nil {
        log.Fatalln(err)
    }
    defer conn.Close()

    for _ = range time.NewTicker(time.Second).C {
        conn.Write([]byte(fmt.Sprintf("%d", time.Now().UnixNano())))
        conn.Write([]byte("\n"))
    }
}

问题记录

如何签名 IP 地址?

在生成服务端公钥过程中, 将 IP 地址填入 Common Name 是无效的(此字段必须域名), 正确做法是:

# 复制 openssl.conf
cp /etc/pki/tls/openssl.cnf .

# 修改以下配置
[ v3_ca ]
subjectAltName=@alternate_names

# 并增加以下新区块
[ alternate_names ]
DNS.1      = localhost
DNS.2      = ...
IP.1       = 127.0.0.1
IP.2       = ...
# 生成 CA 私钥
openssl genrsa -out ca.key 2048
# 生成 CA 证书
openssl req -x509 -new -key ca.key -days 36525 -out ca.crt -config openssl.cnf

# 生成服务端私钥
openssl genrsa -out server.key 2048
# 生成服务端签名请求
openssl req -new -key server.key -out server.csr -config openssl.cnf
# 使用 CA 对服务端签名请求进行签名, 并生成服务端证书
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 36525 -extfile openssl.cnf -extensions v3_ca

# 生成客户端私钥
openssl genrsa -out client.key 2048
# 生成客户端签名请求
openssl req -new -key client.key -out client.csr
# 使用 CA 对客户端签名请求进行签名, 并生成客户端证书
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt -days 36525