戰(zhàn):從configure到動(dòng)態(tài)庫部署的完整避坑指南)
簡介面向Windows平臺(tái)C/C開發(fā)者的OpenSSL 1.1.1c預(yù)編譯庫文件包由Visual Studio 2015編譯生成同時(shí)提供靜態(tài)庫與動(dòng)態(tài)庫兩種鏈接方式且各含32位和64位版本可省去自行配置編譯環(huán)境的繁瑣流程直接集成于工程使用。包體共928個(gè)文件解壓后約43.73MB核心內(nèi)容包括848個(gè)頭文件、16個(gè)lib導(dǎo)入庫、16個(gè)dll運(yùn)行庫以及配套pdb調(diào)試符號(hào)和少量exe工具目錄劃分清晰便于按需選擇。已有1630人學(xué)習(xí)下載適用于需要快速接入HTTPS、TLS加密通信等場景的開發(fā)者。資源內(nèi)含完整頭文件和lib/dll靜態(tài)庫版本適合發(fā)布無外部依賴程序動(dòng)態(tài)庫版本則便于更新升級(jí)同時(shí)附帶pdb符號(hào)文件方便VC2015環(huán)境下進(jìn)行源碼級(jí)調(diào)試與問題定位。 做服務(wù)端網(wǎng)關(guān)和嵌入式移植的人多半都經(jīng)歷過這樣的場景業(yè)務(wù)代碼在別人的機(jī)器上跑得好好的換到自己的環(huán)境一鏈接就報(bào)OpenSSL版本不匹配或者干脆編譯出一堆莫名其妙的符號(hào)錯(cuò)誤。我自己就吃過不少虧尤其是需要把特定版本的openssl庫編譯成動(dòng)態(tài)庫、再分發(fā)到多臺(tái)機(jī)器時(shí)版本錯(cuò)一個(gè)字母后面全是坑。今天借著一個(gè)具體的版本號(hào)OpenSSL_1_1_1c把編譯這件事從頭到尾捋一遍包括為什么選這個(gè)版本、configure參數(shù)怎么定、make之后怎么驗(yàn)證以及幾個(gè)非常容易忽略的細(xì)節(jié)。這篇內(nèi)容適合幾類人看一是正在做C/C服務(wù)端、被系統(tǒng)自帶OpenSSL版本坑過的人二是需要給客戶或生產(chǎn)環(huán)境定制openssl庫的運(yùn)維或移植工程師三是剛接觸openssl源碼編譯、想搞懂每一步在干什么的初學(xué)者。我會(huì)把關(guān)鍵參數(shù)和避坑經(jīng)驗(yàn)全部攤開講保證你看完能直接拿來用。1. 為什么要自己編譯openssl庫而不是直接用系統(tǒng)自帶的1.1 版本策略和生命周期是繞不開的坎先聊版本。OpenSSL的版本命名有自己一套邏輯1.1.1系列屬于長期支持版本官方維護(hù)周期拉得比較長。1.1.1c是1.1.1系列里的第三個(gè)維護(hù)版本修復(fù)了一批此前版本的功能性問題同時(shí)沒有引入像3.0那樣的大版本架構(gòu)調(diào)整。對(duì)于很多存量項(xiàng)目來說這一版是“夠用且穩(wěn)”的典型代表。但系統(tǒng)自帶的openssl往往不是你想要的那個(gè)版本。Ubuntu 20.04自帶的是1.1.1系列某個(gè)后續(xù)小版本CentOS 7帶的是1.0.2CentOS 8帶的是1.1.1。看起來都是“差不多能用”但你一旦用到某些只在特定小版本里修復(fù)過的接口或者反過來你依賴的第三方庫是照著某個(gè)舊版本頭文件編譯的版本差異就會(huì)在運(yùn)行時(shí)捅出簍子。最典型的就是ABI兼容問題——頭文件聲明的結(jié)構(gòu)體布局變了代碼還能編譯通過運(yùn)行的時(shí)候卻會(huì)靜默崩潰或內(nèi)存越界。所以自己編譯的最大意義在于把openssl版本牢牢鎖死讓開發(fā)、測試、生產(chǎn)三套環(huán)境的庫完全一致。這樣后面出現(xiàn)任何SSL相關(guān)的問題你至少能確定不是版本漂移引起的。1.2 自己編譯的另一個(gè)理由裁剪和定制openssl的編譯系統(tǒng)非常靈活你可以選擇只編譯靜態(tài)庫、只編譯動(dòng)態(tài)庫、或者兩者都出可以選擇啟用或禁用某些算法可以指定安裝路徑完全脫離系統(tǒng)目錄還可以在編譯階段就決定要不要thread支持、要不要zlib壓縮。這些是二進(jìn)制發(fā)行包很難給的自由度。比如有的嵌入式板子內(nèi)存緊張你可以把不需要的算法裁剪掉顯著減小庫的體積。我在實(shí)際項(xiàng)目里最常用到的定制就是“指定安裝路徑”和“分離動(dòng)態(tài)庫依賴”。前者方便做綠色部署后者方便做多版本共存。2. 編譯前的環(huán)境準(zhǔn)備工具鏈和依賴檢查2.1 Linux環(huán)境perl、make、編譯器一個(gè)都不能少openssl的構(gòu)建系統(tǒng)基于perl腳本生成Makefile所以perl是硬依賴。在Debian/Ubuntu上先檢查基礎(chǔ)工具sudo apt update sudo apt install -y perl gcc make如果是CentOS/RHEL系包管理器換成yum或dnf包名一樣。這里要特別提醒不要圖省事跳到下一步先跑一下版本檢查確認(rèn)perl版本不是太老。有些老版本的perl在處理openssl 1.1.1c的Configure腳本時(shí)會(huì)有細(xì)微的語法兼容問題表現(xiàn)為“莫名奇妙的字符串拼接錯(cuò)誤”。我吃到過一次虧最后迫不得已升級(jí)了perl才通過。2.2 Windows環(huán)境兩個(gè)不同工具鏈的選擇在Windows上編譯openssl常見路線有兩條MSVC和MinGW。如果你最終要把openssl集成到Visual Studio項(xiàng)目里走M(jìn)SVC如果你是在MinGW環(huán)境下做交叉開發(fā)走M(jìn)inGW。兩條路線用的命令不同但思路一致。MSVC路線最麻煩的是環(huán)境變量。你需要在“開發(fā)人員命令提示符”里操作或者手動(dòng)調(diào)用vcvars64.bat把cl、nmake這些命令加進(jìn)PATH。MinGW路線稍友好一些裝好MSYS2后在MSYS2的shell里直接執(zhí)行perl Configure即可。不過MinGW生成的庫API層面和MSVC生成的基本一致但鏈接時(shí)細(xì)節(jié)有差異比如導(dǎo)入庫格式所以選定一條路線就別隨意混用。2.3 源碼獲取與完整性校驗(yàn)源碼用wget或curl去官方站點(diǎn)下載我習(xí)慣固定下載到/opt/src這種獨(dú)立目錄避免和其他項(xiàng)目源碼混在一起。cd /opt/src wget https://www.openssl.org/source/openssl-1.1.1c.tar.gz wget https://www.openssl.org/source/openssl-1.1.1c.tar.gz.sha256 sha256sum -c openssl-1.1.1c.tar.gz.sha256這里真心建議下載完務(wù)必做一次SHA256校驗(yàn)。openssl這名字太招搖了被投毒的概率比其他開源庫高得多。校驗(yàn)通過后再繼續(xù)不要跳過這一步。3. 配置階段的關(guān)鍵選擇configure參數(shù)一文理清3.1 前綴、openssldir和庫類型進(jìn)入解壓目錄后第一步是執(zhí)行Configure。openssl 1.1.1c支持config腳本和Configure腳本兩種入口config本質(zhì)是Configure的封裝會(huì)自動(dòng)探測系統(tǒng)類型日常直接用config就好。我推薦一組比較穩(wěn)妥的配置./config --prefix/opt/openssl-1.1.1c --openssldir/opt/openssl-1.1.1c/ssl shared threads zlib no-async--prefix指定安裝根目錄編譯產(chǎn)物會(huì)分bin、lib、include三個(gè)子目錄放到這里。--openssldir單獨(dú)指定運(yùn)行時(shí)配置文件的目錄比如系統(tǒng)默認(rèn)的CA證書路徑就在這個(gè)目錄下。如果你不指定--openssldir默認(rèn)會(huì)跟隨--prefix。這一步是很多人忽略的想做到完全隔離就必須把這兩個(gè)參數(shù)都寫清楚。shared表示生成動(dòng)態(tài)庫同時(shí)默認(rèn)也會(huì)生成靜態(tài)庫這一點(diǎn)在1.1.1系列里不需要額外加no-static。threads啟用多線程安全支持幾乎所有的服務(wù)端程序都需要建議保留。zlib啟用對(duì)壓縮算法的支持如果你不需要HTTPS里的壓縮協(xié)商可以考慮去掉以減少依賴。no-async則在老編譯器環(huán)境下很有用——它關(guān)閉了異步IO相關(guān)的硬件支持避免編譯某些架構(gòu)的匯編代碼時(shí)報(bào)錯(cuò)。3.2 關(guān)于no-asm和匯編的取舍openssl為了性能帶有大量匯編優(yōu)化的密碼學(xué)代碼。在某些交叉編譯或老舊的編譯器環(huán)境下這些匯編代碼會(huì)引入“不支持的操作碼”或“版本過舊”的錯(cuò)誤。遇到這種情況一個(gè)保底方案就是加no-asm參數(shù)強(qiáng)制關(guān)閉匯編用純C實(shí)現(xiàn)。代價(jià)是性能會(huì)有所下降特別是AES和SHA這類高頻算法差距可能在兩三倍左右。但在性能要求不高的內(nèi)部工具或早期驗(yàn)證環(huán)境完全能接受。記住一句話穩(wěn)定性優(yōu)先時(shí)絕不硬扛匯編優(yōu)化。3.3 交叉編譯需要額外指定target如果你是在x86的機(jī)器上編譯arm平臺(tái)要用的openssl配置方式完全不同。./config不能自動(dòng)識(shí)別交叉編譯器需要手動(dòng)指定target./Configure linux-armv4 --prefix/opt/openssl-arm --openssldir/opt/openssl-arm/ssl shared threads no-asm no-async同時(shí)需要設(shè)置好編譯器的環(huán)境變量比如CROSS_COMPILEarm-linux-gnueabihf-openssl的構(gòu)建系統(tǒng)會(huì)把它拼到gcc前面去。交叉編譯里最容易出的問題是編譯器前綴寫錯(cuò)或沒導(dǎo)出到環(huán)境變量結(jié)果它去調(diào)了本機(jī)的gcc生成了x86的.o文件鏈接時(shí)再報(bào)一堆“wrong architecture”。每次交叉編譯前先執(zhí)行echo檢查一下環(huán)境變量的值。4. 編譯安裝全流程實(shí)操從make到ldconfig4.1 編譯和安裝的標(biāo)準(zhǔn)動(dòng)作配置完成后直接開始編譯。建議按需指定并行度但不要太貪心make -j8在一臺(tái)8核的機(jī)器上比較舒服如果模版機(jī)內(nèi)存小-j4更穩(wěn)妥。openssl編譯過程中偶發(fā)“內(nèi)存不足導(dǎo)致的編譯器崩潰”降低并行度基本能解決。make -j8 make install_sw這里特意用了install_sw而不是install。它只安裝軟件部分頭文件、庫、二進(jìn)制不會(huì)安裝文檔和man手冊(cè)能在自動(dòng)化構(gòu)建時(shí)省不少時(shí)間。如果你只是編庫自用推薦這個(gè)如果希望機(jī)器上能直接man openssl再用完整install。安裝完成后檢查一下產(chǎn)物ls -l /opt/openssl-1.1.1c/lib/正常情況下你應(yīng)該看到libcrypto.a libcrypto.so - libcrypto.so.1.1 libcrypto.so.1.1 libssl.a libssl.so - libssl.so.1.1 libssl.so.1.14.2 自帶測試最好別跳過的自測環(huán)節(jié)make結(jié)束之后不管時(shí)間多緊建議至少跑一次自帶的測試套件make testopenssl的測試套件非常龐大完整的跑完會(huì)花很久。如果你只是想快速驗(yàn)證這個(gè)編譯產(chǎn)物基本可用可以只跑核心的smoke測試比如make test TESTStest_ssl test_x509 test_verify重點(diǎn)是test_ssl和test_verify因?yàn)檫@兩個(gè)測試能證明編譯出來的庫自帶一套自洽的TLS實(shí)現(xiàn)證書校驗(yàn)邏輯沒有硬傷。要是測試報(bào)錯(cuò)了先別急檢查一下是不是因?yàn)橹碍h(huán)境里已經(jīng)裝了別的版本openssl導(dǎo)致環(huán)境變量污染。這一條我在后面問題排查部分還會(huì)展開。4.3 鏈接驗(yàn)證用一段小代碼確認(rèn)庫可用安裝完我們可以寫一個(gè)最簡單的C程序驗(yàn)證庫的可用性#include stdio.h #include openssl/ssl.h #include openssl/opensslv.h int main() { printf(OpenSSL version: %s\n, OpenSSL_version(OPENSSL_VERSION)); printf(Compile time version: %s\n, OPENSSL_VERSION_TEXT); SSL_library_init(); return 0; }編譯命令gcc -o ver ver.c -I/opt/openssl-1.1.1c/include -L/opt/openssl-1.1.1c/lib -lssl -lcrypto這里務(wù)必加上-L指定庫目錄否則gcc默認(rèn)會(huì)去/usr/lib找系統(tǒng)自帶的庫。編譯通過后執(zhí)行LD_LIBRARY_PATH/opt/openssl-1.1.1c/lib ./ver運(yùn)行能看到類似OpenSSL 1.1.1c 28 May 2019的輸出就說明編譯產(chǎn)物完全可用。4.4 ldconfig配置與運(yùn)行時(shí)庫識(shí)別如果上面的LD_LIBRARY_PATH只用于臨時(shí)測試那了解即可。若要讓系統(tǒng)在運(yùn)行階段穩(wěn)定識(shí)別到/opt下的這個(gè)openssl庫方法有兩種。臨時(shí)、不推薦但最快的方式是用LD_LIBRARY_PATH導(dǎo)出路徑。這種方式對(duì)當(dāng)前終端生效但在systemd服務(wù)、cron任務(wù)里很容易失效排查起來非常痛苦。更優(yōu)雅的方式是寫一個(gè)conf文件讓動(dòng)態(tài)加載器長期記住這個(gè)路徑echo /opt/openssl-1.1.1c/lib /etc/ld.so.conf.d/openssl111c.conf ldconfig ldconfig -p | grep opensslldconfig -p的輸出里能看到libssl.so.1.1和libcrypto.so.1.1兩條記錄說明動(dòng)態(tài)庫已經(jīng)被系統(tǒng)識(shí)別。不過還要提醒一句如果你把openssl的庫路徑加入全局動(dòng)態(tài)庫搜索路徑系統(tǒng)里其他依賴舊版本openssl的程序會(huì)優(yōu)先加載到新版本凡是編譯時(shí)沒有用rpath鎖定的程序都可能因此出現(xiàn)ABI不兼容。這也是我一直提倡“構(gòu)建時(shí)鎖定rpath”的原因。5. 常見問題與排查技巧實(shí)錄5.1 openssl version mismatch是怎么回事很多人在編譯完自己的openssl后運(yùn)行第三方程序時(shí)遇到類似“openssl version mismatch. built against 30000070, you have 30500050”的報(bào)錯(cuò)。其中“built against”表示程序編譯時(shí)鏈接的openssl版本是3.0.7“you have”表示運(yùn)行時(shí)加載到的openssl版本是3.5.0.50。這個(gè)報(bào)錯(cuò)的本質(zhì)是運(yùn)行時(shí)加載的庫不是你編譯時(shí)鏈接的庫。解決辦法就是回到本文開頭提到的“鎖定版本”思路。用ldd 程序名看它到底加載了哪個(gè)路徑下的libssl.so和libcrypto.so然后通過LD_LIBRARY_PATH或rpath把它指向你編譯好的、與編譯期一致的版本。對(duì)應(yīng)到我們編譯的1.1.1c如果第三方庫是拿1.1.1c的頭文件編譯的運(yùn)行時(shí)就必須讓系統(tǒng)加載到libssl.so.1.1對(duì)應(yīng)的那個(gè)產(chǎn)物。尤其當(dāng)系統(tǒng)已經(jīng)裝了OpenSSL 3.x時(shí)動(dòng)態(tài)鏈接器會(huì)優(yōu)先找版本號(hào)更高的新庫因此不加干預(yù)就很容易串版本。5.2 verify -cafile的困境證書路徑找不對(duì)編譯完openssl后很多人會(huì)用openssl verify -cafile去驗(yàn)證自簽證書結(jié)果總是返回“unable to get local issuer certificate”。這時(shí)候首先要區(qū)分是證書鏈問題還是庫的問題。用我們剛編譯出來的版本顯式指定CA文件路徑/opt/openssl-1.1.1c/bin/openssl verify -CAfile /path/to/your-ca.pem server.crt如果這樣能通過說明編譯出來的庫是好的問題出在默認(rèn)的--openssldir路徑。你之前配置--openssldir時(shí)目錄下的certs和private目錄是空的沒有任何系統(tǒng)CA證書因此老的CA路徑找不到。生產(chǎn)環(huán)境要么把CA證書手動(dòng)放到openssldir/certs要么通過SSL_CERT_FILE環(huán)境變量顯式指定CA文件位置。不要因?yàn)檫@一步報(bào)錯(cuò)就去懷疑編譯有問題。5.3 Windows環(huán)境下DLL找不到在Windows上用MSVC編譯完openssl你會(huì)在bin目錄看到libssl-1_1-x64.dll和libcrypto-1_1-x64.dll。把這兩個(gè)DLL復(fù)制到exe同目錄或者把openssl的bin目錄加到系統(tǒng)PATH否則程序啟動(dòng)時(shí)直接報(bào)“找不到libcrypto-1_1-x64.dll”彈窗。另外一個(gè)常見現(xiàn)象是程序里明明已經(jīng)復(fù)制了DLL但vs調(diào)試時(shí)仍然顯示找不到。這時(shí)檢查是不是把x64和x86的DLL搞混了。-x64的DLL不能給Win32的程序用反之亦然。我見過不少人在x86的exe里塞了x64的DLL折騰一晚上沒找到原因。5.4 匯編錯(cuò)誤與編譯器兼容性在老的編譯環(huán)境上比如gcc 4.x編譯openssl 1.1.1c經(jīng)常遇到類似“Error:x86_64 is not recognized”的匯編錯(cuò)誤這通常是因?yàn)榫幾g器把代碼生成了不支持的指令格式。最直接的規(guī)避方式是重新Configure并加上no-asm二樓已經(jīng)說過。如果你必須保留匯編性能那就得升級(jí)編譯器。在嵌入式工具鏈中尤其要記住先驗(yàn)證工具鏈支持度再?zèng)Q定要不要碰匯編優(yōu)化。5.5 第三方語言綁定加載openssl失敗很多場景下編譯完openssl后還要給Python或Node.js等高級(jí)語言用。比如Python的ssl模塊依賴_ssl擴(kuò)展而_ssl會(huì)鏈接系統(tǒng)openssl或自定義openssl。當(dāng)報(bào)出“module ‘ssl’ has no attribute ‘OPENSSL_VERSION_1_1_1’”之類的錯(cuò)誤時(shí)八成是Python擴(kuò)展鏈接的庫和你預(yù)期的庫不一致。這是屬于動(dòng)態(tài)庫優(yōu)先級(jí)問題發(fā)生在系統(tǒng)openssl 3.x和自定義1.1.1c共存的環(huán)境下。解決思路就是重建擴(kuò)展模塊或在構(gòu)建時(shí)顯式設(shè)置LIBRARY_PATH和CPATH讓Python擴(kuò)展能找到正確版本的庫。6. 我的一點(diǎn)實(shí)操心得反復(fù)編譯過很多次openssl之后我現(xiàn)在的標(biāo)準(zhǔn)流程是先確定版本再確定平臺(tái)工具鏈然后一次性寫好Configure參數(shù)和安裝路徑編譯一次通過后立刻整理一份文檔記錄當(dāng)前的參數(shù)組合、編譯機(jī)器信息、產(chǎn)物哈希值。這樣任何一臺(tái)新機(jī)器需要重新編譯時(shí)我不用再靠回憶去復(fù)現(xiàn)舊配置。有個(gè)小技巧是把Configure參數(shù)和版本號(hào)一起寫進(jìn)安裝路徑的注釋或說明文件里。比如在/opt/openssl-1.1.1c/README_BUILD.txt里寫上完整的Configure命令、編譯日期、編譯用戶。這個(gè)習(xí)慣幫我在半年后重新排查舊項(xiàng)目時(shí)節(jié)省了大量時(shí)間。還要特別強(qiáng)調(diào)一下openssl 1.1.1c本身的API在編譯階段不會(huì)給太多提醒很多問題都潛伏在運(yùn)行時(shí)。所以編譯完成后的自測環(huán)節(jié)千萬別跳過尤其是make test以及你自己寫的那段最小TLS握手程序。這兩步過了這塊庫才算真正“裝好”了。本文還有配套的精品資源點(diǎn)擊獲取