50從零開始用Rust編寫nginx,原來TLS證書還可以這麼申請

来源:https://www.cnblogs.com/wmproxy/p/18033496/wmproxy50
-Advertisement-
Play Games

TLS證書在當今互聯網中處於最重要的一環,他保護著我們的隱私數據的安全,也是最流行的加密方式之一。所以TLS證書的快速部署對於小而美的應用能讓其快速的落地使用。 ...


wmproxy

wmproxy已用Rust實現http/https代理, socks5代理, 反向代理, 負載均衡, 靜態文件伺服器,websocket代理,四層TCP/UDP轉發,內網穿透等,會將實現過程分享出來,感興趣的可以一起造個輪子

項目地址

國內: https://gitee.com/tickbh/wmproxy

github: https://github.com/tickbh/wmproxy

設計目標

讓系統擁有acme的能力,即可以領取Let's Encrypt的證書簽發,快速實現上線部署。

acme是什麼?

ACME(Automated Certificate Management Environment)是一個用於自動化管理SSL/TLS證書的協議。它通過自動獲取、自動更新和自動拒絕等功能,可以大大提高SSL證書的管理和更新效率,降低錯誤風險,提高網站的安全性和穩定性。

當ACME伺服器發佈不安全的SSL證書時,可以通過ACME協議自動拒絕證書,確保網站始終使用安全的SSL證書。此外,ACME協議還支持自動續期功能,這意味著在證書到期之前,系統可以自動申請並獲取新的證書,從而避免了因證書過期而導致的網站訪問中斷或安全風險。

acme的定義

acme是一個可以自動獲取 TLS證書的協議,acmev1已經被正式棄用,現行的acme在rfc8555定義。其中定義了SSL如何獲取的整個過程,包括其中最重要的許可權鑒定。

以下是兩種acme判定許可權擁有者的鑒權方式,以下是wmproxy.net做為功能變數名稱來舉例。

HTTP-01 方式鑒定

HTTP-01 的校驗原理是訪問給你功能變數名稱指向的 HTTP 服務增加一個臨時 location,Let’s Encrypt 會發送 http 請求到 http://wmproxy.net/.well-known/acme-challenge/wmproxy.net 就是被校驗的功能變數名稱,TOKEN 是 ACME 協議的客戶端負責放置的文件,在這裡 ACME 客戶端就是 acme-lib。Let’s Encrypt 會對比 TOKEN 是否符合預期,校驗成功後就會頒發證書。不支持泛功能變數名稱證書。成功後我們就可以擁有TLS證書了。

  • 優點
    配置簡單通用
    任何DNS服務商均可

  • 缺點
    需要依賴HTTP伺服器
    集群會無法申請的可能
    不支持泛功能變數名稱

DNS-01 方式鑒定

在 ACME DNS 質詢驗證的自動化中,以下是一些關鍵步驟:

  1. 生成一個 DNS TXT 記錄,如_acme-challenge
  2. 將 TXT 記錄添加到 DNS 區域中。
  3. 通知 Let's Encrypt 驗證 DNS 記錄。
  4. 等待 Let's Encrypt 驗證完成。
  5. 如果驗證成功,則生成證書。
  6. 刪除 DNS TXT 記錄。

此方法不需要你的服務使用 Http 服務,並且支持泛功能變數名稱證書。

  • 優點
    不需要HTTP伺服器
    支持泛功能變數名稱

  • 缺點
    各DNS服務商均不一致

acme在保證安全的情況下縮短了TLS證書的申請流程,可以自動化的進行部署,極大的緩解因證書過期帶來的麻煩。

代碼實現

依賴:acme-lib
改造:之前是確定配置證書及密鑰後直接生成完整的TLS信息TlsAcceptor,那麼現在在未申請到證書前,不能確定完整的TlsAcceptor,需要對初始化對象進行重新改造處理。
源碼:wrap_tls_accepter
定義:

/// 為了適應acme, 重新改造Acceptor進行封裝處理
#[derive(Clone)]
pub struct WrapTlsAccepter {
    pub last: Instant,
    pub domain: Option<String>,
    pub accepter: Option<TlsAcceptor>,
}

同樣添加accept方法

#[inline]
    pub fn accept<IO>(&self, stream: IO) -> io::Result<Accept<IO>>
    where
        IO: AsyncRead + AsyncWrite + Unpin,
    {
        self.accept_with(stream, |_| ())
    }

    pub fn accept_with<IO, F>(&self, stream: IO, f: F) -> io::Result<Accept<IO>>
    where
        IO: AsyncRead + AsyncWrite + Unpin,
        F: FnOnce(&mut ServerConnection),
    {
        if let Some(a) = &self.accepter {
            Ok(a.accept_with(stream, f))
        } else {
            self.check_and_request_cert()
                .map_err(|_| io::Error::new(io::ErrorKind::Other, "load https error"))?;
            Err(io::Error::new(io::ErrorKind::Other, "try next https error"))
        }
    }

accepter未初始化時,我們將會試圖檢查證書,查看是否能簽發證書。

此處我們為了避免併發中,重覆多次請求導致請求數過多導致的服務不可用,我們此處定義了全局靜態變數。

lazy_static! {
    static ref CACHE_REQUEST: Mutex<HashMap<String, Instant>> = Mutex::new(HashMap::new());
}

在檢查的時候,我們只允許一段時間內僅有一個請求進入申請證書的流程,其它的請求全部返回錯誤:

let mut map = CACHE_REQUEST
    .lock()
    .map_err(|_| io::Error::new(io::ErrorKind::Other, "Fail get Lock"))?;
if let Some(last) = map.get(self.domain.as_ref().unwrap()) {
    if last.elapsed() < Duration::from_secs(30) {
        return Err(io::Error::new(io::ErrorKind::Other, "等待上次請求結束").into());
    }
}
map.insert(self.domain.clone().unwrap(), Instant::now());

然後我們對該功能變數名稱發起證書簽名請求,此處我們會迴圈卡住整個線程,而非非同步的請求,所以我們這裡用了thread::spawn而非tokio::spawn

let obj = self.clone();
thread::spawn(move || {
    let _ = obj.request_cert();
});

以下是請求證書的函數:

fn request_cert(&self) -> Result<(), Error> {
    // 使用let's encrypt簽發證書
    let url = DirectoryUrl::LetsEncrypt;
    let path = Path::new(".well-known/acme-challenge");
    if !path.exists() {
        let _ = std::fs::create_dir_all(path);
    }

    // 使用記憶體的存儲結構,存儲自己做處理
    let persist = MemoryPersist::new();

    // 創建目錄節點
    let dir = Directory::from_url(persist, url)?;

    // 設置請求的email信息
    let acc = dir.account("[email protected]")?;

    // 請求簽發的功能變數名稱
    let mut ord_new = acc.new_order(&self.domain.clone().unwrap_or_default(), &[])?;

    let start = Instant::now();
    // 以下功能變數名稱的鑒權,需要等待let's encrypt確認信息
    let ord_csr = loop {
        // 成功簽發,跳出迴圈
        if let Some(ord_csr) = ord_new.confirm_validations() {
            break ord_csr;
        }

        // 超時30秒,認為失敗了
        if start.elapsed() > Duration::from_secs(30) {
            println!("獲取證書超時");
            return Ok(());
        }

        // 獲取鑒權方式
        let auths = ord_new.authorizations()?;

        // 以下是HTTP的請求方法,本質上是請求token的url,然後返回正確的值
        // 此處我們用的是臨時伺服器
        //
        // /var/www/.well-known/acme-challenge/<token>
        //
        // http://mydomain.io/.well-known/acme-challenge/<token>
        let chall = auths[0].http_challenge();

        // 將token存儲在目錄下
        let token = chall.http_token();
        let path = format!(".well-known/acme-challenge/{}", token);

        // 獲取token的內容
        let proof = chall.http_proof();

        Helper::write_to_file(&path, proof.as_bytes())?;

        // 等待acme檢測時間,以ms計
        chall.validate(5000)?;

        // 再嘗試刷新acme請求
        ord_new.refresh()?;

    };

    // 創建rsa的密鑰對
    let pkey_pri = create_rsa_key(2048);

    // 提交CSR獲取最終的簽名
    let ord_cert = ord_csr.finalize_pkey(pkey_pri, 5000)?;

    // 下載簽名及證書,此時下載下來的為pkcs#8證書格式
    let cert = ord_cert.download_and_save_cert()?;
    Helper::write_to_file(
        &self.get_cert_path().unwrap(),
        cert.certificate().as_bytes(),
    )?;
    Helper::write_to_file(&self.get_key_path().unwrap(), cert.private_key().as_bytes())?;
    Ok(())
}

在其中,我們跟acme伺服器的時候我們需要架設臨時文件伺服器以使acme訪問我們http伺服器的時候http://mydomain.io/.well-known/acme-challenge/<token>能正確的返回正常的請求,我們將在綁定tls的時候,如果沒有該證書信息時,我們將自動添加一個.well-known/acme-challenge的location以啟用https的驗證:

pub async fn bind(
    &mut self,
) -> ProxyResult<(Vec<Option<WrapTlsAccepter>>, Vec<bool>, Vec<TcpListener>)> {
    // ...
    for value in &mut self.server {
        // ... 
        if has_acme {
            let mut location = LocationConfig::new();
            let file_server = FileServer::new(
                ".well-known/acme-challenge".to_string(),
                "/.well-known/acme-challenge".to_string(),
            );
            location.rule = Matcher::from_str("/.well-known/acme-challenge/").expect("matcher error");
            location.file_server = Some(file_server);
            value.location.insert(0, location);
        }
    }
    Ok((accepters, tlss, listeners))
}

以啟用遠程acme能訪問該鏈接的能力,也就意味著我們不能將敏感信息放置在".well-known/acme-challenge"目錄下麵,也就是我們使用MemoryPersist的原因。

測試是否可行

因為http-01的方式必須使acme能訪問我們的伺服器,所以此時測試需要公網環境下進行測試:
我們配置如下文件,reverse.toml:

# 反向代理相關,七層協議為http及https
[http]

# 反向代理中的具體服務,可配置多個多組
[[http.server]]
bind_addr = "0.0.0.0:80"
bind_ssl = "0.0.0.0:443"
up_name = "auto1.wmproxy.net"
root = ""

[[http.server.location]]
rule = "/"
static_response = "I'm Ok {client_ip}"

此時佈置在我們的auto1.wmproxy.net的伺服器上,我們運行

wmproxy run -c reverse.toml

此時當我們訪問https://auto1.wmproxy.net的請求的時候,將會觸發證書申請,成功後證書將放置在".well-known"下麵,下次啟動伺服器的時候我們將自動載入已請求的tls證書以提供https服務。

頻繁限制問題

在let's encrypt中,如果有早過5次成功後,需要2天後才能繼續申請,他將無限返回429,得註意控制申請證書的頻率。

總結

TLS證書在當今互聯網中處於最重要的一環,他保護著我們的隱私數據的安全,也是最流行的加密方式之一。所以TLS證書的快速部署對於小而美的應用能讓其快速的落地使用。

點擊 [關註][在看][點贊] 是對作者最大的支持


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 本文主要討論了訂單履約系統的應用架構。首先提出了訂單履約系統的三大核心能力,分別是履約服務表達、履約調度和物流配送。文中還詳細介紹了訂單履約系統的應用架構,包括C端履約服務和B端管理模塊,以及領域層的能力。 ...
  • 軟體架構是成功開發軟體產品的基礎。精心設計的軟體架構可以大大提高系統的質量。它還有助於降低出錯風險,並使將來添加新特性和功能變得更加容易。在這篇博文中,我將為您列出 2024 年最值得一讀的軟體架構書籍,以及 2024 年將出版哪些有趣的軟體架構書籍。當然,這些書籍中的大多數也是 2023 年最佳軟 ...
  • Redis主要在記憶體中操作數據,記憶體是一種臨時存儲,一旦斷電(或者硬體故障、軟體錯誤等),記憶體中的數據就會煙消雲散。有的同學會說,數據不是會保存到硬碟嗎?是的,但是還是可能會有一些數據來不及寫入硬碟,這是Redis的持久化機制導致的。而且,即使Redis將全部數據都及時保存到了硬碟,硬碟出現問題也可... ...
  • 簡介 抽象工廠模式是一種創建型設計模式,它提供了一種創建一系列相關或相互依賴對象的介面,而無需指定它們具體的類。抽象工廠模式將一組具有共同主題的單個工廠封裝起來,它提供介面用於創建相關或依賴對象的家族,而不需要指定具體的類。 抽象工廠模式包含以下幾個核心角色: 抽象工廠(Abstract Facto ...
  • 本文分享自華為雲社區《java代碼實現非同步返回結果如何判斷非同步執行完成》,作者: 皮牙子抓飯。 在許多應用程式中,我們經常使用非同步操作來提高性能和響應度。在Java中,我們可以使用多線程或者非同步任務來執行耗時操作,並且在後臺處理過程完成後獲取結果。但是,在使用非同步操作時,我們通常需要知道非同步任務何時 ...
  • 本文介紹基於C++語言GDAL庫,為CreateCopy()函數創建的柵格圖像添加更多波段的方法。 在C++語言的GDAL庫中,我們可以基於CreateCopy()函數與Create()函數創建新的柵格圖像文件。其中,CreateCopy()函數需要基於一個已有的柵格圖像文件作為模板,將模板文件的各 ...
  • 項目簡介 IExcel 用於優雅地讀取和寫入 excel。 避免大 excel 出現 oom,簡約而不簡單。 特性 一行代碼搞定一切 OO 的方式操作 excel,編程更加方便優雅。 sax 模式讀取,SXSS 模式寫入。避免 excel 大文件 OOM。 基於註解,編程更加靈活。 設計簡單,註釋完 ...
  • 在zookeeper中,follower也可以接收客戶端連接,處理客戶端請求,本文將分析follower處理客戶端請求的流程: 讀請求處理 寫請求轉發與響應 follower接收轉發客戶端請求 網路層接收客戶端數據包 leader、follower都會啟動ServerCnxnFactory組件,用來 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...