Android 使用 OpenSSL 库

Posted by Piasy on May 27, 2023
本文是 Piasy 原创,发表于 https://blog.piasy.com,请阅读原文支持原创 https://blog.piasy.com/2023/05/27/Android-OpenSSL/

背景

不久前我在封装 libdatachannel Android 库中介绍了通过 prefab 在项目中引入 OpenSSL 依赖,当时确实是解决了项目所需,不过后来碰到了一个兼容性问题,在 Android 5.x 系统上,加载依赖了 OpenSSL 的库时,会 crash:

E  dlopen("/xxxxxx/lib/arm/libdatachannel.so", RTLD_LAZY) failed: 
dlopen failed: cannot locate symbol "X509_getm_notBefore" referenced by "libdatachannel.so"...

是说找不到 X509_getm_notBefore 这个符号,但是 apk 打包的 libcrypto.so 里确实是有这个符号的。

经过一番搜索,找到了解释,而且就是在 prefab 的 GitHub issue 里找到的:

The libcrypto.so in the OpenSSL package definitely has that symbol. IIRC before M system libraries would be loaded preferentially to libraries in the app if they had the same name, so we’re getting /system/lib/libcrypto.so instead of the one in the app :(

是说在 Android M 之前的系统上,因为和系统带的 OpenSSL 库符号重名冲突了,所以导致了问题。

prefab 官方始终没有解决这个问题,但这个 issue 下有人也提出了规避方式:要么就是把符号重命名,避免冲突;要么就是使用 OpenSSL 静态库。

看到这里我不禁想到,既然系统里有 OpenSSL 库,为什么我们不能直接用呢?自己打包一份 OpenSSL 的库,反倒是把包体积变大了。

使用系统 OpenSSL 库

其实我们自己项目的编译和链接过程,只要提供 OpenSSL 的头文件和 so 库文件,就能成功,至于这些文件是怎么来的,无论是自己编译的,还是从 Android 设备上导出来,都可以。然后把我们自己的库编译出来之后,打包 apk 的时候别带着 OpenSSL 库,而是去加载系统里的 libcrypto.so 和 libssl.so 就行。此外,OpenSSL 的接口是向前兼容的,比如我们使用 Android API 19 使用的 OpenSSL 版本的头文件和库,在新版的 Android 系统上应该也是没有兼容性问题的,当然前提是我们的代码没有用到新版本 OpenSSL 添加的接口。

不过在验证这个方案的时候,也遇到了问题:

E  library "/system/lib64/libcrypto.so" ("/system/lib64/libcrypto.so") 
needed or dlopened by "/apex/com.android.art/lib64/libnativeloader.so" is not accessible 
for the namespace: xxxxxxx

原来是从 Android 7.0 开始,系统的动态库,只有写在 /system/etc/public.libraries.txt 里的库,普通 app 才能够使用,否则加载就会失败,报上面的错误。更详细的说明,可以看这个 reddit 讨论

其实之前在嵌入式设备上有遇到过类似的情况,当时是需要使用 libtinyalsa.so。

所以使用系统 OpenSSL 库还是不行。

使用 OpenSSL 静态库

我们当然可以制作一个 prefab 的 OpenSSL 静态库组件,不过这个工作量有点大,我希望能先直接本地编译出 OpenSSL 静态库之后,在 CMake 项目里使用。编译命令我是基于 subins2000/android-openssl-cmake 稍作修改的

在引入编译好的 OpenSSL 库的过程中,我又遇到了最初的 find_package 报错问题:

CMake Error at /Users/piasy/tools/android-sdk/cmake/3.22.1/share/cmake-3.22/
Modules/FindPackageHandleStandardArgs.cmake:230 (message):
Could NOT find OpenSSL, try to set the path to OpenSSL root folder in the
system variable OPENSSL_ROOT_DIR (missing: OPENSSL_CRYPTO_LIBRARY
OPENSSL_INCLUDE_DIR) (Required is at least version "1.1.0")
Call Stack (most recent call first):
/Users/piasy/tools/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/
FindPackageHandleStandardArgs.cmake:594 (_FPHSA_FAILURE_MESSAGE)
/Users/piasy/tools/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/
FindOpenSSL.cmake:574 (find_package_handle_standard_args)
deps/libsrtp/CMakeLists.txt:77 (find_package)

就算按照提示设置了 OPENSSL_ROOT_DIR 变量,也还是不管用,最终还是经过一番搜索,找到了两种解决方案。

其实我们也不是必须使用 CMake 提供的 FindOpenSSL.cmake,自己写一个就好了。

方案一能支持使用 CONFIG 模式的 find_package 调用,把如下内容写入 OpenSSLConfig.cmake

if (NOT TARGET OpenSSL::Crypto)
    add_library(OpenSSL::Crypto STATIC IMPORTED)
    set_target_properties(OpenSSL::Crypto PROPERTIES
            IMPORTED_LOCATION "${OPENSSL_ROOT_DIR}/lib/libcrypto.a"
            INTERFACE_INCLUDE_DIRECTORIES "${OPENSSL_ROOT_DIR}/include"
            INTERFACE_LINK_LIBRARIES ""
            )
endif ()

if (NOT TARGET OpenSSL::SSL)
    add_library(OpenSSL::SSL STATIC IMPORTED)
    set_target_properties(OpenSSL::SSL PROPERTIES
            IMPORTED_LOCATION "${OPENSSL_ROOT_DIR}/lib/libssl.a"
            INTERFACE_INCLUDE_DIRECTORIES "${OPENSSL_ROOT_DIR}/include"
            INTERFACE_LINK_LIBRARIES ""
            )
endif ()

并把 OpenSSLConfig.cmake 所在的路径设置给 OpenSSL_DIR,或者添加到 CMAKE_PREFIX_PATH 里:

set(OpenSSL_DIR path-to-openssl-config)
list(APPEND CMAKE_PREFIX_PATH path-to-openssl-config)

这样就可以通过 find_package(OpenSSL REQUIRED CONFIG) 找到 OpenSSL 库了。

不过 libdatachannel 的 CMakeLists.txt 里用的是 MODULE 模式,也就是 find_package(OpenSSL REQUIRED),上述配置方式不管用,依然还是会调用到 CMake 提供的 FindOpenSSL.cmake,依然会报上面的错。

方案二就可以解决,把和上面 OpenSSLConfig.cmake 的一样的内容写入 FindOpenSSL.cmake,并把它所在的路径添加到 CMAKE_MODULE_PATH 中即可:

list(APPEND CMAKE_MODULE_PATH path-to-find-openssl)

CONFIG 模式和 MODULE 模式,其实只是 CMake 查找时使用的 cmake 文件名规则不一样,一个是 <LibraryName>Config.cmake 或者 <lower-case-library-name>-config.cmake,一个是 Find<LibraryName>.cmake,用哪个都可以。


欢迎大家加入 Hack WebRTC 星球,和我一起钻研 WebRTC。

piasy-knowladge-planet