文章目錄
- 前言
- 生成證書
- 申請免費的證書
- 使用Go語言生成自簽CA證書
- https的客戶端和服務端
- 服務端代碼
- 客戶端代碼
- tls的客戶端和服務端
- 服務端
- 客戶端
前言
在公網中,我想加密傳輸的數據。(1)很自然,我想到了把數據放到http的請求中,然后通過tls確保數據安全。(2)更進一步,只要數據可以解析,則無需http協議,直接通過tls協議加密傳輸即可。本文分別嘗試了這兩個方案。
嘗試實現方案之前,我們考慮需要實現哪些內容。(1)如何獲取證書。(2)golang中如何實現一個https的客戶端和服務器。(3)golang中如何實現一個tls的客戶端和服務器。(4)http的request和response的構建,發送和解析。(5)對于客戶端, 應用層(http)是否應該復用網絡層(tcp)的連接; 哪些需求下不能復用; (6)不考慮傳輸層的網絡細節。
注:本文不涉及相關內容的背景知識介紹。本文完整代碼見倉庫。
生成證書
如果有已經購買的域名,可以申請一個免費的通配符證書,便于日常使用。
沒有域名的話:可以通過命令行生成證書,見:windows和linux上證書的增刪查。也可以通過go代碼來創建證書。
申請免費的證書
首先是安裝acme.sh
sudo apt-get -y install socatda1234cao@vultr:~$ curl https://get.acme.sh | sh -s email=da1234cao@163.com% Total % Received % Xferd Average Speed Time Time Time CurrentDload Upload Total Spent Left Speed
100 1032 0 1032 0 0 4384 0 --:--:-- --:--:-- --:--:-- 4410% Total % Received % Xferd Average Speed Time Time Time CurrentDload Upload Total Spent Left Speed
100 216k 100 216k 0 0 715k 0 --:--:-- --:--:-- --:--:-- 715k
[Fri Aug 11 06:17:18 AM UTC 2023] Installing from online archive.
[Fri Aug 11 06:17:18 AM UTC 2023] Downloading https://github.com/acmesh-official/acme.sh/archive/master.tar.gz
[Fri Aug 11 06:17:19 AM UTC 2023] Extracting master.tar.gz
[Fri Aug 11 06:17:19 AM UTC 2023] Installing to /home/da1234cao/.acme.sh
[Fri Aug 11 06:17:19 AM UTC 2023] Installed to /home/da1234cao/.acme.sh/acme.sh
[Fri Aug 11 06:17:19 AM UTC 2023] Installing alias to '/home/da1234cao/.bashrc'
[Fri Aug 11 06:17:19 AM UTC 2023] OK, Close and reopen your terminal to start using acme.sh
[Fri Aug 11 06:17:19 AM UTC 2023] Installing cron job
8 0 * * * "/home/da1234cao/.acme.sh"/acme.sh --cron --home "/home/da1234cao/.acme.sh" > /dev/null
[Fri Aug 11 06:17:19 AM UTC 2023] Good, bash is found, so change the shebang to use bash as preferred.
[Fri Aug 11 06:17:20 AM UTC 2023] OK
[Fri Aug 11 06:17:20 AM UTC 2023] Install success!
為了使用方便,我這里申請一個泛域名證書。我的域名是在阿里云購買的,所以本文僅嘗試獲取阿里云的泛域名證書。
參考:阿里云域名使用ACME自動申請免費的通配符https域名證書、acme.sh 使用泛域名|阿里云DNS |免費申請證書。
大概過程是:AccessKey管理->創建子用戶->允許open API訪問->添加DNS管理權限。將獲取到的AccessKey 和 Secret 寫到acme.sh.env配置文件里面。
export Ali_Key="*****"
export Ali_Secret="*******"
執行source ~/.bashrc
。
然后開始申請證書。
sudo ufw status
sudo ufw allow 80# --debug 參數查看執行過程
# 沒有web服務,80端口空閑, acme.sh 還能假裝自己是一個webserver, 臨時聽在80 端口, 完成驗證
## 如果執行報錯;稍等等會再嘗試;
acme.sh --issue --dns dns_ali -d *.da1234cao.top --standalone --debug[Fri Aug 11 07:07:43 AM UTC 2023] Your cert is in: /home/da1234cao/.acme.sh/*.da1234cao.top_ecc/*.da1234cao.top.cer
[Fri Aug 11 07:07:43 AM UTC 2023] Your cert key is in: /home/da1234cao/.acme.sh/*.da1234cao.top_ecc/*.da1234cao.top.key
[Fri Aug 11 07:07:43 AM UTC 2023] The intermediate CA cert is in: /home/da1234cao/.acme.sh/*.da1234cao.top_ecc/ca.cer
[Fri Aug 11 07:07:43 AM UTC 2023] And the full chain certs is there: /home/da1234cao/.acme.sh/*.da1234cao.top_ecc/fullchain.cer
[Fri Aug 11 07:07:43 AM UTC 2023] _on_issue_success
# 查看證書信息
openssl x509 -noout -text -in '*.da1234cao.top.cer'Signature Algorithm: ecdsa-with-SHA384Issuer: C = AT, O = ZeroSSL, CN = ZeroSSL ECC Domain Secure Site CAValidityNot Before: Aug 11 00:00:00 2023 GMTNot After : Nov 9 23:59:59 2023 GMTSubject: CN = *.da1234cao.top <-----# 查看key信息
openssl ec -noout -text -in '*.da1234cao.top.key'
read EC key
Private-Key: (256 bit)
使用Go語言生成自簽CA證書
這里有比較詳細的介紹:使用Go語言生成自簽CA證書
package certificateimport ("crypto/rand""crypto/rsa""crypto/x509""crypto/x509/pkix""encoding/pem""io/ioutil""math/big""time"
)func Gencertificate(output string) error {// ref: https://foreverz.cn/go-cert// 生成私鑰priv, err := rsa.GenerateKey(rand.Reader, 2048)if err != nil {return err}// x509證書內容var csr = &x509.Certificate{Version: 3,SerialNumber: big.NewInt(time.Now().Unix()),Subject: pkix.Name{Country: []string{"CN"},Province: []string{"Shanghai"},Locality: []string{"Shanghai"},Organization: []string{"httpsDemo"},OrganizationalUnit: []string{"httpsDemo"},CommonName: "da1234cao.top",},NotBefore: time.Now(),NotAfter: time.Now().AddDate(1, 0, 0),BasicConstraintsValid: true,IsCA: false,KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},}// 證書簽名certDer, err := x509.CreateCertificate(rand.Reader, csr, csr, priv.Public(), priv)if err != nil {return err}// 二進制證書解析interCert, err := x509.ParseCertificate(certDer)if err != nil {return err}// 證書寫入文件pemData := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE",Bytes: interCert.Raw,})if err = ioutil.WriteFile(output+"cert.pem", pemData, 0644); err != nil {panic(err)}// 私鑰寫入文件keyData := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY",Bytes: x509.MarshalPKCS1PrivateKey(priv),})if err = ioutil.WriteFile(output+"key.pem", keyData, 0644); err != nil {return err}return nil
}
https的客戶端和服務端
輪子已經有了,net/http。我沒有看到net/http
很好的入門教程。只能看下官方文檔,網上翻翻一些簡單的示例。
下面是一個示例。其中,必須一提的是,http請求結束后,連接可以仍然存在,放到閑置的連接池中。便于后續請求,復用之前的連接。我到源碼里面去看了下,沒太看懂,可見:Golang Http RoundTrip解析。
服務端代碼
啟動服務端后,對于/reflect
路徑的request,構建一個response。注意其中的header和body內容的填充。
func reflect(w http.ResponseWriter, r *http.Request) {log.Println("handle reflect")w.Header().Set("Content-Type", "text/plain; charset=utf-8")w.WriteHeader(http.StatusOK)bodyByte, _ := io.ReadAll(r.Body)log.Println("recv:", string(bodyByte))w.Write(bodyByte)
}func Start() error {listenPort := Conf.ListenPortlistenIp := Conf.ListenIpif listenPort <= 0 || listenPort > 65535 {log.Println("invalid listen port:", listenPort)return errors.New("invalid listen port")}http.HandleFunc("/reflect", reflect)err := http.ListenAndServeTLS(listenIp+":"+strconv.Itoa(listenPort), Conf.Protocol.Https.Certificate, Conf.Protocol.Https.Key, nil)return err
}
客戶端代碼
客戶端可以選擇是否驗證服務端的證書。驗證證書不重要,因為我信任這個域名解析過程(如果DNS沒有被污染的話)。數據可以加密傳輸即可。代碼中使用http.NewRequest
構建一個請求,然后通過http.Client.Do
發送請求(可能會復用之前的連接)。
func New() *http.Client {cli := &http.Client{Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: Conf.SkipVerify},},}return cli
}func DoRequest(cli *http.Client, data []byte) (*http.Response, error) {req, err := http.NewRequest("POST", Conf.Protocol+"://"+Conf.ServerIp+":"+strconv.Itoa(Conf.ServerPort)+"/reflect", bytes.NewBuffer(data))if err != nil {log.Println("fail to consstruct request", err)}return cli.Do(req)
}func PrintResponse(resp *http.Response, err error) {if err == nil {if resp.StatusCode == http.StatusOK {body, _ := ioutil.ReadAll(resp.Body)log.Print(string(body))}log.Println("http status code:", resp.StatusCode)} else {log.Print(err)}
}
tls的客戶端和服務端
當我們的應用層不需要http協議,只需要對應用層的數據進行加密傳輸。我們嘗試下面的代碼(下面代碼中,我手動構建了http的request和response,是為了保證接收到完整的數據后再處理,僅此而已)。 使用的庫是crypto/tls
服務端
func TLSDataHandle(conn net.Conn) {for {// 讀取requestioBuf := bufio.NewReader(conn)req, err := http.ReadRequest(ioBuf)if err != nil {log.Println(err)return}defer req.Body.Close()defer conn.Close()bodyByte, _ := io.ReadAll(req.Body)log.Println("recv: ", string(bodyByte))// 構建一個responsebuf := bytes.NewBuffer(nil)buf.WriteString("HTTP/1.1 200 OK\r\n")buf.WriteString("Content-Length: " + strconv.Itoa(len(bodyByte)) + "\r\n")buf.WriteString("\r\n")buf.Write(bodyByte)// 發送responsebuf.WriteTo(conn)}
}func TLSStart() error {listenPort := Conf.ListenPortlistenIp := Conf.ListenIpif listenPort <= 0 || listenPort > 65535 {log.Println("invalid listen port:", listenPort)return errors.New("invalid listen port")}cert, err := tls.LoadX509KeyPair(Conf.Protocol.Https.Certificate, Conf.Protocol.Https.Key)if err != nil {log.Println("fail to laod x509 key pair", err)}config := &tls.Config{Certificates: []tls.Certificate{cert}}listener, _ := tls.Listen("tcp", listenIp+":"+strconv.Itoa(listenPort), config)for {conn, _ := listener.Accept()go TLSDataHandle(conn)}
}
客戶端
func NewTlsConn() (net.Conn, error) {config := &tls.Config{InsecureSkipVerify: Conf.SkipVerify}return tls.Dial("tcp", Conf.ServerIp+":"+strconv.Itoa(Conf.ServerPort), config)
}func SendRequest(conn net.Conn, data []byte) {// 構造一個請求buf := bytes.NewBuffer(nil)buf.WriteString("POST /no_thing")buf.WriteString(" HTTP/1.1\r\n")buf.WriteString("Content-Length: " + strconv.Itoa(len(data)) + "\r\n")buf.WriteString("\r\n")buf.Write(data)// 發送請求buf.WriteTo(conn)// 讀取回復ioBuf := bufio.NewReader(conn)res, err := http.ReadResponse(ioBuf, nil)if err != nil {log.Println(err)return}defer res.Body.Close()bodyByte, _ := io.ReadAll(res.Body)log.Println(string(bodyByte))
}