新開源HTML5單文件網頁版ACME客戶端,可在線申請Let’s Encrypt、ZeroSSL免費HTTPS多域名通配符泛域名SSL/TLS證書(RSA/ECC/ECDSA)

之前寫了兩篇關於HTTPS證書的文章,一篇是本地自簽證書,另一篇是Let’s Encrypt開始支援免費通配符證書時通過diafygi/gethttpsforfree網頁來申請證書,步驟均很繁瑣;於2022年9月我寫了我開發了一個操作非常簡單的網頁版ACME客戶端:ACME-HTML-Web-Browser-Client,並在GitHubGitee上進行了開源。

整個源碼僅一個靜態HTML網頁文件,可以直接保存到本地使用,或通過在線網址使用;支援向 Let's EncryptZeroSSL 等支援 ACME 協議的證書頒發機構,免費申請獲得用於 HTTPS 的 SSL/TLS 域名證書(RSAECC/ECDSA),支援多域名和通配符泛域名;只需在現代瀏覽器上操作即可獲得 PEM 格式純文本的域名證書,不依賴作業系統環境,無需下載和安裝軟體,無需註冊登錄,純手動操作,只專註於申請獲得證書這一件事

證書過期風險提醒: 由於本網頁客戶端只能純手動操作,不支援自動續期,需注意在證書過期前重新生申請新證書(免費證書普遍90天有效期,屆時只需重複操作一遍即可),或使用 acme.sh 等客戶端自動化續期。

網頁客戶端測試截圖

開源項目的起源

我自己主要是在Windows裡面用的多,並不介意甚至更偏向於純手動操作獲得證書,但官方ACME客戶端列表裡面的能單純提供符合我需求的寥寥無幾:依賴特定作業系統環境、需要安裝特定的運行環境、要自己寫程式碼開發、甚至有的還必須捆綁線上生產環境;而我需求僅僅是單純的想獲得一個證書文件而已,但帶來的是一整套全家桶,很難專註於只申請獲得證書這一件事。

常見的ACME客戶端要麼是需要Bash腳本環境,要麼就是不同開發語言的源程式碼或者第三方庫需要自己寫程式碼;早先Let’s Encrypt的列表裡面還有提供網頁版的客戶端列表(我在裡面找到的gethttpsforfree),現在官方因為「一些瀏覽器內 ACME 客戶端可用,但我們未在此處列出它們,因為它們鼓勵手動續訂工作流程,這會導致糟糕的用戶體驗並增加錯過續訂的風險」而不再展示網頁版客戶端列表,我很失望;也有部分支援Windows的桌面程式可以直接使用,比如win-acme,不過看了文檔也很難上手;有部分客戶端或網站甚至會要求註冊並登陸賬戶才能使用,還幫你管理私鑰(存儲到他們的伺服器,恨不得把通訊錄都拿走),哈 ~ 呸 ~~

長時間我都是使用fszlin/certes這個.Net的庫來簽發證書,自己C#寫的一個控制台程式,雙擊程式就能直接到達驗證階段,配置好DNS驗證記錄就能申請到證書文件,但要給別人用就麻煩了,要改C#程式碼;更重要的是EXE程式還有一個信任的問題,雙擊一個EXE程式需要承擔的風險很大;網頁版就完全不同了,網頁本身就帶有很大程度的開放性,我們完全可以審查程式碼是否有加料,網頁天然適合做UI,人機交互的友好性遠超腳本控制台程式,對於用不上自動化續期、或自動化在伺服器進行部署證書的需求,在網頁上進行申請是最理想的操作方式;比如diafygi/gethttpsforfree我們完全可以看到他的網頁源碼,還能通過瀏覽器控制台完全監控到網路數據流向,最為簡單和可靠,雖然這個網頁也是開源的,但它需要的操作太繁瑣了,早先我還給他提了個我自己在用的簡化簽名操作的方法 issues/164

為了能用上一個符合我心裡預期的並且操作簡單的網頁版ACME客戶端,依託於現代瀏覽器對RSAECC加密、簽名的直接支援,因此我決定自己寫!好在以前積累了點基礎的密碼學知識,編寫本客戶端不存在知識盲點,很順利的寫完了全部HTML程式碼,並進行了開源。

項目地址

GitHub: //github.com/xiangyuecn/ACME-HTML-Web-Browser-Client
Gitee: //gitee.com/xiangyuecn/ACME-HTML-Web-Browser-Client

在線使用本客戶端:
中國訪問gitee.io: //xiangyuecn.gitee.io/acme-html-web-browser-client/ACME-HTML-Web-Browser-Client.html
不太穩定github.io: //xiangyuecn.github.io/ACME-HTML-Web-Browser-Client/ACME-HTML-Web-Browser-Client.html

使用方法

這裡介紹的是完整的操作流程,請根據步驟進行操作,很容易就能申請得到證書文件;本文圖片中顯示的數據都是演示用的並非真實數據。

第一步:選擇Let’s Encrypt、ZeroSSL或其他證書頒發機構

根據上面的項目地址,打開本網頁客戶端(下載保存唯一的這一個html文件並雙擊打開、或者使用在線網址);只要是支援ACME協議的證書頒發機構,都可以在第一步操作中填寫對應地址,目前默認提供了Let's EncryptZeroSSL兩家的地址,根據自己需要直接選擇即可。

不同證書頒發機構需要的操作不一定相同,請根據提示進行操作;Let's Encrypt直接到第二步操作,ZeroSSL由於他們的ACME服務對瀏覽器支援很不友好存在跨域問題,需要多操作幾下(複製源碼到他們服務地址頁面中運行,消除跨域問題)。

選擇證書頒發機構

第二步:證書配置,填寫域名

在網頁的第二步中填寫需要申請證書的域名(支援多域名、通配符),和密鑰等資訊配置;不同證書頒發機構需要填寫的配置不一定相同,請根據提示進行操作。

填寫域名列表: 比如要給你的域名test123.com申請證書,在域名中可以直接填寫:test123.com, *.test123.com,帶了一個通配符,這申請出來的證書在test123.comwww.test123.comapp.test123.com等域名都可以使用。注意:如果需要xxx.app.test123.com這種域名也能使用,需增加填寫*.app.test123.com類似這種才行。

證書私鑰: 建議直接點擊創建新的RSA私鑰。

ACME賬戶私鑰: 如果你之前申請過,建議填寫之前用過的私鑰,沒有申請過也同樣的創建新的RSA私鑰。

ACME聯繫郵箱: 填寫一個你常用的郵箱,證書頒發機構會在證書到期前給你推送郵件提醒續期,亂填郵箱收不到提醒。

EAB憑據: 有些ACME服務會要求提供外部帳號綁定憑據(External Account BindingexternalAccountBindingexternalAccountRequired),比如ZeroSSL:你可以在ZeroSSL的管理控制台的 Developer 中獲得此憑據,所以你需要先註冊一個ZeroSSL的帳號;Let’s Encrypt沒有此選項。

配置證書

第三步:完成域名所有權的驗證

根據網頁內顯示的第三步內容,給每個域名配置好合適的域名所有權驗證方式;常見的驗證方式有:DNS解析中配置TXT記錄進行驗證、上傳文件到伺服器進行驗證,通配符域名一般僅支援DNS驗證;等全部配置完成後開始驗證,驗證通過後就能下載得到證書了。

推薦使用DNS驗證方式,只需要登錄你的域名解析管理後台,在域名解析中填寫子域名TXT記錄就行,簡單快捷,並且不容易出錯;比如*.test123.com,我們需要給子域名_acme-challenge.test123.com添加一條TXT記錄(這一個子域名下可以添加多條TXT記錄)。

通過上傳文件來驗證,不太推薦這個方式,因為必須要有伺服器,不然ACME訪問不到文件就會驗證不通過;為了讓上傳的文件能正常訪問,可能需要在伺服器內進行一些配置(請自行搜索解決辦法),測試URL地址你自己也能正常訪問後,才能開始驗證。

驗證域名所有權

第四步:下載保存證書PEM文件

等域名所有權驗證通過後,網頁會顯示第四步,提供證書文件下載,下載保存好;有3個文件需要下載(都是純文本,可以用記事本打開):

  1. 證書文件(含完整證書鏈)
  2. 證書私鑰文件
  3. 日誌記錄文件,下次申請時直接拖拽此文件進頁面,自動填寫相同配置

證書文件的後綴.pem可以改成.crt.cer,這樣在Windows裡面可以直接雙擊打開查看此證書。

最後將下載保存的證書和私鑰配置到你的網站中即可,不同系統程式的配置方法不同,請自行查找對應配置方法。

下載保存證書

源碼中一些可以把玩的程式碼

為了在瀏覽器上實現RSAECC簽名操作,源碼中用純js程式碼實現了:

  • PKCS#1的解析,PEM格式公鑰和私鑰
  • PKCS#8的解析和生成,PEM格式公鑰和私鑰
  • PKCS#10的生成,PEM格式證書請求CSR文件(Certification Request)

對應的功能封裝在本客戶端內的 X509 對象中,用 X509.CreateCSR 來生成CSR,用 X509.KeyGenerate 來創建PEM格式密鑰,用 X509.KeyParse 來解析PEM格式密鑰,用 X509.KeyExport 來導出PEM格式密鑰。

這些功能都是根據相應的標準用js程式碼在二進位層面上實現的,二進位數據操作封裝在了 ASN1 對象中:實現了 ASN.1 標準的二進位解析和封包,使用 ASN1.ParsePEM 方法可以解析任意的PEM格式密鑰或證書。

請打開本網頁客戶端,在瀏覽器控制台內執行一下程式碼觀賞:

console.log("生成一個512位的RSA私鑰 PKCS#8:");
X509.DefaultType2_RSA="512";
privateKey=await new Promise(function(resolve,reject){
	X509.KeyGenerate("RSA", function(pem){resolve(pem)}, function(err){reject(new Error(err))})
});
console.log(privateKey);

console.log("解析一個pem格式密鑰:");
keyInfo=await new Promise(function(resolve,reject){
	X509.KeyParse(privateKey, function(info){resolve(info)}, function(err){reject(new Error(err))})
});
console.log(keyInfo);

console.log("導出私鑰的公鑰 PKCS#8:");
publicKey=X509.KeyExport(keyInfo, true);
console.log(publicKey);

console.log("用私鑰創建一個證書請求CSR PKCS#10:");
csr=await new Promise(function(resolve,reject){
	X509.CreateCSR(keyInfo, "test123.com"
		, ["test.com","*.test.com","*.app.test.com","test123.com","*.test123.com"]
		, function(csr){resolve(csr)}, function(err){reject(new Error(err))})
});
console.log(csr);

console.log("用ASN1解析CSR得到二進位結構:");
csrASN1=ASN1.ParsePEM(csr);
console.log(csrASN1);

console.log("用ASN1解析私鑰得到二進位結構:");
privateKeyASN1=ASN1.ParsePEM(privateKey);
console.log(privateKeyASN1);

console.log("用ASN1解析公鑰得到二進位結構:");
publicKeyASN1=ASN1.ParsePEM(publicKey);
console.log(publicKeyASN1);

【END】