Go 編程: 證書自動化與幾大郵件運營商的(接收/投遞)安全漏洞

最近手擼了一個純 Go 的郵件系統,在證書配置上使用了 autocert包進行證書自動化。同時在與幾大郵件運營上接收與投遞測試的過程當中發現了對方的一些安全漏洞。本文就證書自動化與郵件運營商這些安全漏洞進行闡述。

原文連接git

證書自動化

原理

實現證書自動化,首先固然得感謝 letsencrypt.org 簽發的免費證書。github

簡單解釋一下 letsencrypt.org 簽發證書的原理。 letsencrypt.org 共提供了 4 種校驗(challenge)方式, 分別是:golang

  • HTTP-01 challenge
  • DNS-01 challenge
  • TLS-SNI-01 challenge
  • TLS-ALPN-01 challenge

其中校驗方式(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 成功。

  • 公衆號請關注:一艘慢船

qrcode_for_gh_106a756d9c99_258 (1).jpg

相關文章
相關標籤/搜索