最近手擼了一個純 Go 的郵件系統,在證書配置上使用了
autocert
包進行證書自動化。同時在與幾大郵件運營上接收與投遞測試的過程當中發現了對方的一些安全漏洞。本文就證書自動化與郵件運營商這些安全漏洞進行闡述。
原文連接git
實現證書自動化,首先固然得感謝 letsencrypt.org 簽發的免費證書。github
簡單解釋一下 letsencrypt.org 簽發證書的原理。 letsencrypt.org 共提供了 4 種校驗(challenge)方式, 分別是:golang
其中校驗方式(TLS-SNI-01)因爲安全緣由已廢棄,代替方案就是TLS-ALPN-01。雖然有多種校驗(challenge)方式,可是其基本原理是相同的,即驗證所聲明域名資源的可寫權。安全
HTTP-01 challenge 過程,首先 acme
客戶端向 letsencrypt.org 服務請求一個驗證令牌(token), 再將該令牌寫入http://<YOUR_DOMAIN>/.well-known/acme-challenge/<TOKEN>
路徑。這樣 letsencrypt.org 服務經過訪問該路徑來確認 http 資源的可寫權。服務器
DNS-01 challenge 過程,首先 acme
客戶端向 letsencrypt.org 服務請求一個驗證具體的 DNS TXT 記錄值, 並再將記錄值添加到_acme-challenge.<YOUR_DOMAIN>
解析記錄中。這樣 letsencrypt.org 服務經過請求_acme-challenge.<YOUR_DOMAIN>
的 TXT 記錄值來驗證 DNS 資源的可寫權。dom
TLS-ALPN-01 challenge 過程,ALPN (Application Layer Protocol Negotiation)是TLS的擴展,我也不熟不冒充專家,留給讀者本身了。不過基礎原理是相同的。測試
每種校驗方式的優缺點,能夠參考官方文檔: challenge-types.加密
對於開發人員而言,快速實現證書自動化,一般會選擇 HTTP-01 challenge 方式。具體實現代碼很是簡單:spa
package main import ( "context" "io" "log" "net/http" "syscall" "github.com/x-mod/httpserver" "github.com/x-mod/routine" "github.com/x-mod/tlsconfig" "golang.org/x/crypto/acme/autocert" ) func main() { certs := &autocert.Manager{ Prompt: autocert.AcceptTOS, HostPolicy: autocert.HostWhitelist("your-domain"), Cache: autocert.DirCache("your-local-certs-cache-dir"), Email: "your-email-address", } srv := httpserver.New( httpserver.Address(":80"), httpserver.HTTPHandler(certs.HTTPHandler(nil)), ) srvs := httpserver.New( httpserver.Address(":443"), httpserver.TLSConfig(tlsconfig.New( tlsconfig.GetCertificate(certs.GetCertificate), )), httpserver.HTTPHandler( http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { io.WriteString(w, "Hello, world!\n") }), ), ) if err := routine.Main( context.TODO(), routine.ExecutorFunc(srvs.Serve), routine.Go(routine.ExecutorFunc(srv.Serve)), routine.Signal(syscall.SIGINT, routine.SigHandler(func() { srv.Close() srvs.Close() }))); err != nil { log.Println(err) } }
將以上代碼相關配置參數更改成具體配置便可,固然服務運行的公網IP與域名指向必須首先配置好。.net
本次測試發現的問題,均與證書相關。這些問題深入影響了其郵件接收與投遞的安全性,但願本文能引發相關郵件運營商重視並解決其安全漏洞,給用戶提供更加安全的郵箱服務。
郵件接收與投遞的協議是 SMTP 協議,也是不一樣郵件運營商之間交互的關鍵協議。一般 SMTP 協議均服務於 25 端口上, 因爲最開始 SMTP 協議運行在明文上,因此爲了增強 SMTP 協議的安全性,增長了一個STARTTLS
命令。
命令STARTTLS
主要作什麼呢?
簡單的說就是在一個已創建的 TCP 常規鏈接上,經過該命令的方式,進行C/S端的同步升級,升級爲 TLS 鏈接。這個過程和常見的直接監聽 TLS 不一樣,是發生在已創建的 TCP 鏈接上。
如何將 TCP 常規鏈接升級爲 TLS 鏈接呢?
其實也很簡單,不過須要 C/S 端均增長相應的 TLS 證書配置,並開啓加密握手操做便可。寫成代碼以下:
服務端
import "crypto/tls" //tls.Server(conn net.Conn, config *tls.Config) tlsConn := tls.Server(conn, config) if err := tlsConn.Handshake(); err != nil { //TODO } //Upgrade OK
客戶端
import "crypto/tls" //tls.Client(conn net.Conn, config *tls.Config) tlsConn = tls.Client(conn, config)
TLS的過程都在 Handshake
裏了。一旦證書配置錯誤,不管是服務端仍是客戶端,tls.Config
一旦配置錯誤,都會致使握手失敗。如今咱們來看看幾大郵件運營商握手失敗的問題。
先說郵件接收有問題的郵件運營商: 網易郵箱。國內最先開始郵箱服務的運營商,犯了一個很是低級的證書配置錯誤,致使全部外部郵件進入網易郵箱不能經過 TLS 安全連接進行投遞,只能經過明文投遞。
看一下,網易郵箱 163.com
在接收郵件時,報的問題日誌:
STARTTLS: x509: certificate is valid for *.163.com, 163.com, not 163mx02.mxmail.netease.com
問題很明顯。
再說騰訊郵箱,測試郵箱域名 qq.com
. 經過我的 qq 郵箱,發送一封郵件到本身手擼的郵件服務器上, 以 example.com
爲例。配置好 example.com
的 dns mx 記錄到個人郵箱接收服務域名(mx.example.com
)上. 出現問題日誌:
acme/autocert: missing server name
.
很明顯,這個錯誤來自於 autocert 包,至於爲何會報這個錯誤,就是由於騰訊郵箱客戶端投遞時沒有設置證書對應的服務域名。用代碼表示就是:
import "crypto/tls" //tls.Client(conn net.Conn, config *tls.Config) tlsConn = tls.Client(conn, &tls.Config{ ServerName: "", //設置爲服務域名 })
出現相似騰訊郵箱的投遞問題的還有 outlook.com
郵箱。
對比gmail.com
郵箱,STARTTLS
則握手成功。
如何解決此類對方投遞問題
固然最好時投遞方本身,修復該漏洞。固然也能夠在服務的接收端,作一點修改,對與此類證書請求服務域名是空的,默認填上服務域名。
func GetCertificate(defaultServerName string, fn func(hello *tls.ClientHelloInfo) (*tls.Certificate, error)) func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { return func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { glog.V(4).Infof("client server name: %s", hello.ServerName) if hello.ServerName == "" { hello.ServerName = defaultServerName glog.V(4).Infof("set default server name: %s", hello.ServerName) } return fn(hello) } }
這樣就能夠在接收修復客戶端不帶服務端證書域名的問題。
測試了一下 騰訊郵箱發到 Gmail 郵箱, 收到的郵件是經過 TLS 投遞,可見 Gmail 一樣在接收端修復了這個問題,保證 STARTTLS
成功。