简介:适配安卓ARM64-v8a架构的OpenSSL加密库,面向使用NDK开发安卓原生应用的工程师。预编译产物包含完整的头文件目录与arm64-v8a动态库libcrypto.so、libssl.so,支持TLS/SSL安全传输协议、RSA/ECC非对称加解密、数字签名和证书解析,可直接集成进C与C++工程,省去自行交叉编译与配置的繁琐流程;若需国密算法,也可基于现有头文件自行补丁扩展。目录以openssl_arm64-v8a为根,内含标准openssl子目录、include头文件夹与lib库文件夹,结构清晰,便于NDK构建脚本引用。资源包共111个文件,以106个C语言头文件为主体,另有少量C源文件、演示入口与辅助配置,整体仅319KB,非常轻量易用。已有17人学习,适合需要在安卓原生层实现HTTPS通信、安全存储、密钥交换或证书校验的中高级开发者;兼容Android 5.0及以上系统,支持CMake与ndk-build方式导入,并附有简易调用示例,接入门槛较低。 这一套东西,说起来其实就是四个字:交叉编译。也就是在Windows或者Linux电脑上,把OpenSSL的源码编译成Android能用的ARM64版本。整个过程踩坑不少,但弄通之后,你会发现它真的就是个体力活,核心难点就俩:一是搞清楚Android NDK的交叉编译环境怎么配,二是不要在编译命令上犯低级错误。
在这篇文章里,我会把整个流程完整过一遍,包括为什么要用OpenSSL、为什么强调ARM64、工具链怎么搭、编译参数怎么填、动态库怎么引用,以及我实际操作中遇到的那些让人抓狂的问题。如果你正被“在Android里跑HTTPS却报SSL握手失败”折磨,或者想给native层加加密功能,这篇文章应该能帮你少走很多弯路。
1. 为什么要给Android单独编译ARM64版OpenSSL
1.1 系统自带OpenSSL和你需要的OpenSSL不是一回事
很多刚入门的朋友会有个困惑:Android系统底层明明自带OpenSSL(实际上现在叫BoringSSL,是谷歌维护的一个OpenSSL分支),为什么还要自己编译一份?
答案很简单:系统自带的库是给系统用的,不是给你的App用的。Android的API级别对native库的链接非常严格,你在NDK里编译的so文件,只能依赖Android系统提供的公共native API。系统内部那些库(包括BoringSSL)大多藏在/system/lib64下面,而且谷歌明确不推荐、也不保证你能直接链接它们。就算你强行dlopen,ROM一升级就可能崩,兼容性完全没法保证。
所以,如果你想在自己的App里使用OpenSSL的功能,比如做HTTPS双向认证、自定义加密算法、生成证书、或者跟服务器做特殊的TLS握手,那就必须自己编译一份OpenSSL的动态库带进App里。
1.2 为什么必须是指定ARM64架构
先说结论:你的设备是什么CPU架构,你就得编译对应架构的库。目前市面上绝大多数手机、平板、电视盒子,芯片架构都是ARM64(也就是arm64-v8a)。这是2014年之后Android设备的主流形态,几乎覆盖了所有你叫得出名字的主流机型。
你要是只编一个32位的ARM库(armeabi-v7a),在64位设备上也能跑,但性能会打折扣,而且谷歌Play从2019年8月开始要求所有App必须支持64位架构,所以直接用arm64是最省心的方案。
当然,如果你的App还用到了x86模拟器测试,那还得单独编x86_64版本。不过生产环境直接盯死arm64-v8a就对了。
1.3 这个项目里给谁用
这套编译产物主要服务于以下场景:
- 你的Android App里有native代码(C/C++),需要直接调用OpenSSL的API做加密通信;
- 你要用JNI在Java/Kotlin层和native层之间传递数据,需要完整的头文件来写JNI封装;
- 你不想每次都在自己电脑上重复编译,需要一个标准的、带头文件的库文件包,直接丢给团队其他人使用。
所以,这个项目的本质就是:把OpenSSL源码通过Android NDK工具链,编译成arm64-v8a架构的动态链接库(libssl.so和libcrypto.so),同时保留完整的头文件,方便开发者在自己的代码里引用。
2. 编译环境的搭建与工具选型
2.1 必须准备的工具清单
在实际动手之前,先把环境准备好。我的推荐组合如下:
| 工具 | 版本建议 | 说明 |
|---|---|---|
| Ubuntu 20.04/22.04 | 64位 | 编译交叉编译的最佳平台,Windows也能做但坑更多 |
| Android NDK | r21及以后版本 | 包含交叉编译所需的工具链和sysroot |
| OpenSSL源码 | 1.1.1系列或3.x系列 | 目前主流版本,注意3.x部分配置参数有变化 |
| Perl | 系统自带或apt安装 | OpenSSL编译脚本依赖Perl |
| Make | 系统自带 | 编译执行器 |
我个人强烈建议在Linux环境或者Windows Subsystem for Linux(WSL)里做编译,而不是直接用Windows CMD。原因很简单:OpenSSL的Configure脚本是基于Unix工具链设计的,在Windows上还要额外装MSYS2或Cygwin,光是配环境就够你折腾半天。
2.2 为什么用NDK r21+而不是更老的版本
老版本的NDK(比如r17之前)用的是独立的工具链(standalone toolchain),你需要手动执行make-standalone-toolchain.sh脚本生成一个独立编译环境。新版本NDK(r18以后)已经移除了这个方式,改用内置的toolchain.cmake,直接在Configure的时候指定sysroot和编译器路径就行,配置更简洁,也不容易出错。
我用的是NDK r23b,搭配OpenSSL 1.1.1w,这个组合实测下来非常稳。如果你用的是OpenSSL 3.x,配置参数略有不同,后面我会提到。
2.3 下载并解压源码包
这一步没什么技术含量,但要注意路径里不要有空格和中文,否则后面编译会出各种莫名其妙的问题。
# 下载OpenSSL源码(这里以1.1.1w为例) wget https://www.openssl.org/source/openssl-1.1.1w.tar.gz # 解压 tar -xzf openssl-1.1.1w.tar.gz cd openssl-1.1.1w # 下载NDK并解压(假设你已经下载好,这里以ndk-r23b为例) unzip android-ndk-r23b-linux.zip3. 交叉编译OpenSSL的核心配置
3.1 环境变量设置
交叉编译的第一件事,就是把NDK里的编译器路径、sysroot路径这些环境变量设置好。这里我以NDK r23b为例,写下完整的配置:
export NDK_TOOLCHAIN=/path/to/android-ndk-r23b/toolchains/llvm/prebuilt/linux-x86_64 export API_LEVEL=21 export HOST_TAG=linux-x86_64 export PATH=$NDK_TOOLCHAIN/bin:$PATH export CC=$NDK_TOOLCHAIN/bin/aarch64-linux-android21-clang export CXX=$NDK_TOOLCHAIN/bin/aarch64-linux-android21-clang++ export AR=$NDK_TOOLCHAIN/bin/llvm-ar export RANLIB=$NDK_TOOLCHAIN/bin/llvm-ranlib export LD=$NDK_TOOLCHAIN/bin/ld.lld export STRIP=$NDK_TOOLCHAIN/bin/llvm-strip这里的API_LEVEL选择21,是因为Android 5.0(API 21)是第一个原生支持64位ARM架构的版本,用21作为最低版本能覆盖几乎所有设备。如果你的App只支持Android 7.0以上的设备,也可以提高到24或者26,但没必要,反正21编译出来的库在更高版本上都能用。
3.2 Configure参数详解
OpenSSL的Configure脚本是编译的核心,参数怎么填直接决定你能不能用。以1.1.1系列为例,最常用的配置是:
./Configure android-arm64 -D__ANDROID_API__=21 \ --prefix=/usr/local/ssl_arm64 \ --openssldir=/usr/local/ssl_arm64 \ no-shared \ no-tests \ no-unit-test \ no-dso \ no-engine \ no-asm这里面的参数我逐个解释一下:
android-arm64:告诉OpenSSL脚本,我们的目标平台是Android ARM64。这个target定义了体系结构、系统调用约定、字节序等关键信息。-D__ANDROID_API__=21:定义Android API级别为21,这个宏在编译过程中会传递给编译器,确保代码兼容API 21以上的特性。--prefix和--openssldir:指定安装路径,也就是编译完成后头文件和库文件放哪。我习惯放到一个专门的目录,方便后续拷贝。no-shared:编译成静态库而不是动态库。这里需要说明一下,如果你是要直接给App引用,建议用动态库(即去掉no-shared,用shared);no-tests和no-unit-test:跳过测试代码编译,可以大大节省编译时间。no-dso和no-engine:禁用动态加载引擎,Android上一般用不到。no-asm:禁用汇编优化。这个我后面会专门讲,是个关键坑。
3.3 动态库还是静态库
这里我特意提一下动态库和静态库的选择问题。标题里写的是“动态链接库”,所以我编译的时候用的是动态库配置,也就是用shared替代no-shared:
./Configure android-arm64 -D__ANDROID_API__=21 \ --prefix=/usr/local/ssl_arm64 \ --openssldir=/usr/local/ssl_arm64 \ shared \ no-tests \ no-unit-test \ no-engine动态库的好处是App体积小,多个so之间还可以共享代码;坏处是Libcrypto和Libssl之间有依赖顺序问题,加载时需要先加载libcrypto再加载libssl,而且系统可能杀掉你的库导致链接失败。但这些问题都是可以解决的,完全不必担心。
静态库(libcrypto.a)的好处是彻底防依赖问题,整个库都嵌进你的可执行文件里了,缺点就是体积大。
我的建议是:如果你只给自家App用,选动态库就好;如果你要做SDK提供给第三方集成,选静态库更省事,省得别人还要处理依赖问题。
4. 完整编译流程实操
4.1 一次实际的编译过程记录
以下是我在一台干净的Ubuntu 22.04机器上完整编译的实录。我把所有命令行和输出都贴出来,方便你对照。
# 1. 进入OpenSSL源码目录 cd openssl-1.1.1w/ # 2. 设置NDK环境变量(注意换成你自己的路径) export NDK_HOME=/opt/android-ndk-r23b export PATH=$NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin:$PATH # 3. 配置 ./Configure android-arm64 -D__ANDROID_API__=21 \ --prefix=/opt/openssl_arm64 \ --openssldir=/opt/openssl_arm64 \ shared \ no-tests \ no-unit-test \ no-engine \ no-asm # 4. 编译 make -j$(nproc) # 5. 安装到指定目录 make install # 6. 查看编译产出的库文件 ls -l /opt/openssl_arm64/lib/编译完成之后,你会看到/opt/openssl_arm64/lib/下有两个关键的动态库文件:libssl.so和libcrypto.so。同时,/opt/openssl_arm64/include/openssl/目录下会有完整的一堆头文件,包括ssl.h、crypto.h、evp.h、rsa.h、aes.h等常用文件。
4.2 头文件和库文件怎么集成到Android工程
编译好之后,就要把这些文件集成到你的Android Studio项目里了。这一步也是新手容易懵的地方。
首先,在项目的app/src/main下新建一个目录结构,放库文件和头文件:
app/src/main/ ├── cpp/ │ ├── include/openssl/ # 头文件拷贝到这里 │ └── *.cpp # 你的native代码 └── jniLibs/ └── arm64-v8a/ ├── libssl.so # 动态库拷贝到这里 └── libcrypto.so然后,在app/build.gradle里配置externalNativeBuild,告诉CMake头文件路径和库文件路径:
android { defaultConfig { externalNativeBuild { cmake { cppFlags "" arguments "-DANDROID_STL=c++_shared" } } } externalNativeBuild { cmake { path "src/main/cpp/CMakeLists.txt" } } }在CMakeLists.txt里,你需要明确定位头文件和库文件:
cmake_minimum_required(VERSION 3.18.1) project("myopensslapp") # 设置头文件路径 include_directories(${CMAKE_SOURCE_DIR}/include) # 添加动态库依赖 add_library(ssl SHARED IMPORTED) set_target_properties(ssl PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}/libssl.so) add_library(crypto SHARED IMPORTED) set_target_properties(crypto PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}/libcrypto.so) # 你自己的native代码 add_library(myapp SHARED native-lib.cpp) # 链接OpenSSL库 target_link_libraries(myapp ssl crypto)这里有个细节:IMPORTED_LOCATION的路径用的是${CMAKE_SOURCE_DIR}相对于CMakeLists.txt所在目录,不要用绝对路径,否则别人克隆你的项目后编译会失败。
4.3 一个完整的JNI调用示例
光把库放进去还不够,你总得用起来才知道它能不能正常工作。我写一个最简单的JNI调用示例,用OpenSSL计算MD5:
#include <jni.h> #include <openssl/evp.h> #include <string> extern "C" JNIEXPORT jstring JNICALL Java_com_example_myapp_MainActivity_md5FromJNI( JNIEnv* env, jobject /* this */, jstring input) { const char* inputStr = env->GetStringUTFChars(input, nullptr); EVP_MD_CTX* ctx = EVP_MD_CTX_new(); unsigned char digest[EVP_MAX_MD_SIZE]; unsigned int digest_len = 0; EVP_DigestInit_ex(ctx, EVP_md5(), nullptr); EVP_DigestUpdate(ctx, inputStr, strlen(inputStr)); EVP_DigestFinal_ex(ctx, digest, &digest_len); EVP_MD_CTX_free(ctx); env->ReleaseStringUTFChars(input, inputStr); char hex[33]; for (unsigned int i = 0; i < digest_len; i++) { sprintf(hex + i * 2, "%02x", digest[i]); } return env->NewStringUTF(hex); }这个代码虽然简单,但已经把OpenSSL最基本的使用流程串起来了:初始化上下文、执行摘要计算、释放上下文。如果你能跑通这个,说明你的库和头文件都没问题,后面就可以放心写更复杂的逻辑了。
5. 常见错误与排查技巧
5.1 “无法定位程序输入点”类错误
这几乎是Android开发者把OpenSSL集成到App后最常见的启动崩溃错误。你可能会在运行App时看到类似这样的日志:
java.lang.UnsatisfiedLinkError: dlopen failed: cannot locate symbol "SSL_CTX_new" referenced by "..."原因很简单:你的libssl.so和libcrypto.so版本不匹配,或者只加载了libssl.so而没有加载libcrypto.so。OpenSSL的libssl依赖于libcrypto,两个库必须来自同一次编译,并且版本必须一致。
解决办法是在你的AndroidManifest.xml或者代码里确保两个库一起加载:
init { System.loadLibrary("crypto") System.loadLibrary("ssl") }而且注意顺序:必须先加载crypto,再加载ssl。这和库之间的依赖关系有关,顺序反了可能在某些ROM上崩溃。
5.2 Configure时报错“Perl is needed by OpenSSL”
这又是一个经典错误。在你执行./Configure的时候,如果系统没有安装Perl,会直接报错:
Perl is needed by OpenSSL解决办法就一句话:
sudo apt install perl但有个细节你可能想不到:OpenSSL的Configure脚本不仅需要Perl,还需要Perl版本不能太低。有些老版本Ubuntu自带的Perl版本过低,可能也会报错。建议使用Ubuntu 20.04以上的系统,或者手动装一个Perl 5.30以上的版本。
5.3 编译报错找不到arm64架构
如果你在编译过程中看到类似这样的错误:
Error: unrecognized option -mandroid这通常是因为你直接用了系统自带的GCC或者Clang,而不是NDK里的交叉编译器。解决方法是严格按照前面章节的设置,把CC环境变量指向NDK工具链里的aarch64-linux-android21-clang,而不是直接设成clang。
更隐蔽的一个坑是:即使你设置了CC,也要确保PATH里NDK的路径排在前面,否则系统可能会从/usr/bin下找到一个同名的clang来执行。
5.4 no-asm和汇编优化的问题
这是一个非常容易掉进去的坑。我在编译OpenSSL 1.1.1版本时,一开始没有加no-asm参数,结果编译过程中频繁报错:
Error: unknown mnemonic `aesimc`原因是在Android ARM64环境下,OpenSSL默认会启用ARMv8的硬件加速汇编代码,但NDK自带的编译器和汇编器对某些指令的支持并不完整,或者目标CPU级别不匹配。
我的建议是:在Android交叉编译时直接加上no-asm,除非你非常清楚目标CPU的指令集特性。加上no-asm后,代码虽然走的是C语言实现的通用路径,性能会有些损失,但正确性优先。对于绝大多数业务场景,这个性能差别感知不到。
5.5 动态库加载时的“libcrypto.so is smaller than needed”错误
另一个我踩过的坑是,编译的libcrypto.so在实际加载时提示库文件大小不对或校验失败。这通常是因为编译过程中多次编译产生了脏文件,或者源码目录不全。
解决办法是编译前彻底清理:
make clean make distclean ./Configure ...注意:如果你之前用过不同的Configure参数,必须执行make distclean而不是make clean,否则config的缓存会导致你新的配置不生效。
5.6 一个常见排查速查表
为了方便你对照排查,我把这些年碰到的问题整理成了一张表:
| 错误现象 | 最可能原因 | 解决方案 |
|---|---|---|
| dlopen失败找不到符号 | 库版本不匹配或加载顺序错误 | 同时加载crypto和ssl,注意顺序 |
| Configure报Perl错误 | 系统缺少Perl | apt install perl |
| 编译报未知指令 | 未加no-asm | 加no-asm禁用汇编优化 |
| 生成so文件为空或极小 | 编译参数冲突 | make distclean后重新配置 |
| 链接时找不到头文件 | CMake路径配置错误 | 检查include_directories路径 |
| Android 7.0上崩溃 | API级别设置过高 | 用API 21编译 |
| 模拟器无法加载 | 架构不匹配 | 额外编译x86_64版本 |
5.7 集成到Android工程时的注意事项
最后说几个集成阶段特别容易忽略的细节:
第一,不要直接把libssl.so和libcrypto.so丢到同一个目录下重名覆盖。有些新手在拷贝时不小心把两个库都重命名为libnative-lib.so,导致加载时出现不可预知的问题。
第二,不要试图用System.loadLibrary("ssl")去加载系统自带的libssl。因为Android系统本身有一个libssl.so,可能版本和你编译的不一致,加载时会发生命名冲突。我建议你给你的so文件加上自己的前缀,比如libmycrypto.so和libmyssl.so,然后在CMake里改名,这样能避免与系统库冲突。
第三,如果做了混淆或者压缩,记得在proguard-rules.pro里保留JNI方法名:
-keepclasseswithmembernames class * { native <methods>; }不然发布release版本后,你的native方法可能会因为混淆而导致UnsatisfiedLinkError。
6. 方案选型的扩展思考
6.1 要不要升级到OpenSSL 3.x
目前OpenSSL 1.1.1系列已经在2023年9月停止维护,新的安全更新都集中在3.x分支上。如果这是一个新项目,我建议直接使用OpenSSL 3.2或更高版本;如果是老项目,建议尽快做升级规划。
但注意,OpenSSL 3.x的Configure参数略有不同。3.x默认是no-deprecated关闭了部分老API,如果你要用旧接口需要显式打开。另外3.x增加了FIPS模块的概念,如果你需要FIPS认证,底层的编译和配置方式会有变化。
我实测OpenSSL 3.2在Android ARM64上编译时,Configure的命令基本一致,但需要额外注意的是,3.x使用了更高版本的Perl模块,你可能需要先安装Test::More等依赖:
sudo apt install libtest-more-perl6.2 如果只想用加密算法,可以不引入OpenSSL
这是一个容易被忽略的备选方案。如果你的需求只是AES、RSA、HMAC这些常见算法,Android系统自带的javax.crypto和java.security库其实已经够用了。你在Java层完全可以通过Cipher类实现AES加密、通过KeyPairGenerator生成RSA密钥对,根本不需要引入native OpenSSL。
什么时候才需要native OpenSSL?我的经验是:
- 你需要跟第三方C/C++系统做底层TLS握手,且对方要求的TLS版本或算法Android系统不支持;
- 你需要高性能的加解密,Java层有太多对象创建和GC开销;
- 你要在native层做加密,不能每次加解密都做一次JNI调用。
如果只是Java层用,就别折腾native了。
6.3 项目结构上的建议
这个编译项目如果是为了团队使用,我建议把编译脚本和产物单独放到一个仓库里管理,并配合CI流水线。每次OpenSSL发布新版本,只需要更新版本号和重新编译,就能打包出新的库文件,团队成员直接拉取即可。
一个常用的目录结构是:
openssl-android/ ├── build.sh # 一键编译脚本 ├── output/ │ ├── arm64-v8a/ │ │ ├── include/openssl/ │ │ ├── libssl.so │ │ └── libcrypto.so │ └── x86_64/ ├── README.md └── CHANGELOG.md建议给每个架构单独建目录,头文件共用一份即可。库文件命名建议包含版本号,比如libcrypto_1_1_1w.so,避免团队不小心引用到旧版本。
7. 实操心得与收尾
对我个人来说,Android平台下的OpenSSL交叉编译,最大的收获其实不是学会了几条命令,而是理解了整个Android native生态的底层逻辑。你一旦掌握了这套“NDK交叉编译”的方法,不只是OpenSSL,以后想编译FFmpeg、libcurl、zlib这些C/C++库,都是同一套思路,换汤不换药。
最后再分享一个小技巧:编译完成后,用readelf命令检查一下库的架构和依赖,这是确认产物是否正确的快速方法。
readelf -d libssl.so | head -20 readelf -h libssl.so | grep Machine如果Machine显示AArch64,依赖里没有奇怪的系统库,那这个库基本就没问题了。如果你在编译的时候已经跳过了这些坑,那恭喜你,以后在Android里玩加密通信会顺手得多。
本文还有配套的精品资源,点击获取