diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index deba163..c4ad1be 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -89,6 +89,56 @@ jobs: - run: cargo check --locked --lib --all-features + android: + name: Android+Build+Tests + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + rust: + - stable + - beta + - nightly + os: [ ubuntu-latest ] + steps: + - name: Checkout sources + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Install ${{ matrix.rust }} toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust }} + + - name: Setup Android SDK + uses: amyu/setup-android@v4 + with: + sdk-version: 35 + build-tools-version: '35.0.0' + cmake-version: '3.30.5' + ndk-version: '28.0.12433566' + cache-key: 'sdk-35_tools_35.0.0_ndk-28.0.12433566_cmake-3.30.5' + generate-job-summary: true + + - name: CargoNdk + working-directory: ./integration-tests/android + run: | + rustup target add \ + aarch64-linux-android \ + armv7-linux-androideabi \ + x86_64-linux-android \ + i686-linux-android + cargo install cargo-ndk + + - name: cargo build (android; debug; default features) + working-directory: ./integration-tests/android + run: cargo ndk -t armeabi-v7a -t arm64-v8a -t x86 -t x86_64 -o target/ build --locked + + - name: cargo build (android; debug; no default features) + working-directory: ./integration-tests/android + run: cargo ndk -t armeabi-v7a -t arm64-v8a -t x86 -t x86_64 -o target/ build --locked --no-default-features + minimal-versions: name: Check minimum versions of direct dependencies runs-on: ubuntu-latest diff --git a/.github/workflows/smoke-tests.yaml b/.github/workflows/smoke-tests.yaml index 726f33f..21069c4 100644 --- a/.github/workflows/smoke-tests.yaml +++ b/.github/workflows/smoke-tests.yaml @@ -51,3 +51,92 @@ jobs: run: cargo test --locked -- --ignored env: RUST_BACKTRACE: 1 + + smoke-tests-android: + name: Smoke Tests Android + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + rust: + - stable + - beta + - nightly + os: [ ubuntu-latest ] + # Run only on the latest Android API version. + # All the objects and methods used to access the Android KeyStore + # are supported from API 1. No need to check older versions. + api: [ 35 ] + # QEMU2 only support arm64 on x86_64 hosts + # arch: [x86_64, arm64-v8a] + arch: [ x86_64 ] + steps: + - name: Checkout sources + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Install ${{ matrix.rust }} toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust }} + + - name: SetUp JDK 17 + uses: actions/setup-java@v4 + with: + distribution: 'corretto' + java-version: '17' + + - name: Setup Android SDK + uses: amyu/setup-android@v4 + with: + sdk-version: 35 + build-tools-version: '35.0.0' + cmake-version: '3.30.5' + ndk-version: '28.0.12433566' + cache-key: 'sdk-35_tools_35.0.0_ndk-28.0.12433566_cmake-3.30.5' + generate-job-summary: true + + - name: KVM + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - name: Cache + uses: actions/cache@v4 + id: avd-cache + with: + path: | + ~/.android/avd/* + ~/.android/adb* + key: avd-${{ matrix.api }}-${{ matrix.arch }} + + - name: ADV+Snapshot + if: steps.avd-cache.outputs.cache-hit != 'true' + uses: reactivecircus/android-emulator-runner@v2 + with: + working-directory: ./integration-tests/android + api-level: ${{ matrix.api }} + arch: '${{ matrix.arch }}' + target: 'google_apis' + ndk: '28.0.12433566' + cmake: '3.30.5' + emulator-options: -no-window -gpu swiftshader_indirect -no-boot-anim + disable-animations: true + force-avd-creation: false + script: echo "Generated AVD snapshot for caching." + + - name: Build+Tests [${{ matrix.api }}, ${{ matrix.arch }})] + uses: reactivecircus/android-emulator-runner@v2 + with: + working-directory: ./integration-tests/android + api-level: ${{ matrix.api }} + arch: '${{ matrix.arch }}' + target: 'google_apis' + ndk: '28.0.12433566' + cmake: '3.30.5' + emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -no-boot-anim + disable-animations: true + force-avd-creation: false + script: ./gradlew -x lint build test connectedAndroidTest --stacktrace \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 7c19c2f..22868e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -112,6 +112,12 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +[[package]] +name = "bytes" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" + [[package]] name = "cc" version = "1.1.28" @@ -123,6 +129,12 @@ dependencies = [ "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cexpr" version = "0.6.0" @@ -158,6 +170,16 @@ dependencies = [ "cc", ] +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -366,6 +388,28 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + [[package]] name = "jobserver" version = "0.1.32" @@ -400,7 +444,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -528,7 +572,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -683,6 +727,8 @@ dependencies = [ name = "rustls-native-certs" version = "0.8.0" dependencies = [ + "jni", + "libloading", "openssl-probe", "ring", "rustls", @@ -725,6 +771,15 @@ dependencies = [ "untrusted", ] +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scc" version = "2.2.0" @@ -954,6 +1009,16 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -981,13 +1046,31 @@ dependencies = [ "rustix", ] +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -996,7 +1079,22 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", ] [[package]] @@ -1005,28 +1103,46 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -1039,24 +1155,48 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index 206e2ed..a5ee8cb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,8 +27,12 @@ x509-parser = "0.16" [target.'cfg(windows)'.dependencies] schannel = "0.1" -[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies] +[target.'cfg(all(unix, not(target_os = "macos"), not(target_os = "android")))'.dependencies] openssl-probe = "0.1.2" [target.'cfg(target_os = "macos")'.dependencies] security-framework = "2" # 3 requires 1.70 as its MSRV + +[target.'cfg(target_os = "android")'.dependencies] +jni = { version = "0.21.1", default-features = false, features = [] } +libloading = {version = "0.8.5", default-features = false, features = [] } diff --git a/README.md b/README.md index 2cc32e6..3b53e3d 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ this sparingly. # Platform support -This is supported on Windows, macOS and Linux: +This is supported on Windows, macOS, Android and Linux: - On all platforms, the `SSL_CERT_FILE` environment variable is checked first. If that's set, certificates are loaded from the path specified by that variable, @@ -80,6 +80,9 @@ This is supported on Windows, macOS and Linux: The user, admin and system trust settings are merged together as documented by Apple. The [`security-framework`](https://github.com/kornelski/rust-security-framework) crate is used to access the keystore APIs. +- On Android, [`jni`](https://github.com/jni-rs/jni-rs) and + [`libloading`](https://github.com/nagisa/rust_libloading) crates are used to access + the keystore APIs. - On Linux and other UNIX-like operating systems, the [`openssl-probe`](https://github.com/alexcrichton/openssl-probe) crate is used to discover the filename of the system CA bundle. diff --git a/integration-tests/android/.gitignore b/integration-tests/android/.gitignore new file mode 100644 index 0000000..9b375c8 --- /dev/null +++ b/integration-tests/android/.gitignore @@ -0,0 +1,6 @@ +.gradle +.idea +.kotlin +build/ +target/ +local.properties \ No newline at end of file diff --git a/integration-tests/android/Cargo.lock b/integration-tests/android/Cargo.lock new file mode 100644 index 0000000..07e160e --- /dev/null +++ b/integration-tests/android/Cargo.lock @@ -0,0 +1,831 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "android_log-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ecc8056bf6ab9892dcd53216c83d1597487d7dacac16c8df6b877d127df9937" + +[[package]] +name = "asn1-rs" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "bytes" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" + +[[package]] +name = "cc" +version = "1.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16803a61b81d9eabb7eae2588776c4c1e584b738ede45fdbb4c972cec1e9945" +dependencies = [ + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "data-encoding" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" + +[[package]] +name = "der-parser" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "fastrand" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.159" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" + +[[package]] +name = "libloading" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "oid-registry" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9" +dependencies = [ + "asn1-rs", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "proc-macro2" +version = "1.0.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + +[[package]] +name = "rustix" +version = "0.38.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.23.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "415d9944693cb90382053259f89fbb077ea730ad7273047ec63b19bc9b160ba8" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.0" +dependencies = [ + "jni", + "libloading", + "openssl-probe", + "rustls-pemfile", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-native-certs-android-tests" +version = "0.0.1" +dependencies = [ + "android_log-sys", + "jni", + "ring", + "rustls", + "rustls-native-certs", + "rustls-webpki", + "tempfile", + "untrusted", + "webpki-roots", + "x509-parser", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e696e35370c65c9c541198af4543ccd580cf17fc25d8e05c5a242b202488c55" + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tempfile" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "thiserror" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "webpki-roots" +version = "0.26.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841c67bff177718f1d4dfefde8d8f0e78f9b6589319ba88312f567fc5841a958" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "x509-parser" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror", + "time", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/integration-tests/android/Cargo.toml b/integration-tests/android/Cargo.toml new file mode 100644 index 0000000..cb1970a --- /dev/null +++ b/integration-tests/android/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "rustls-native-certs-android-tests" +version = "0.0.1" +edition = "2021" + +[features] +default = [] + +[dependencies] +jni = { version = "0.21", default-features = false } +rustls = { version = "0.23", default-features = false, features = ["std", "ring"] } +rustls-native-certs = { path = "../../", default-features = false, features = [] } +android_log-sys = { version = "0.3", default-features = false, features = [] } +ring = "0.17" +rustls-webpki = "0.102" +tempfile = "3.13" +untrusted = "0.9" +webpki-roots = "0.26" +x509-parser = "0.16" + +[lib] +crate-type = ["cdylib"] +path = "src/main/rust/lib.rs" diff --git a/integration-tests/android/README.md b/integration-tests/android/README.md new file mode 100644 index 0000000..bae0320 --- /dev/null +++ b/integration-tests/android/README.md @@ -0,0 +1,12 @@ +Android Instrumentation Tests +============================= + +The project runs smoketest and compare_mozilla tests on a +Android Virtual Device (AVD) emulator or physical device. + +Connect through adb shell to a AVD or physical device and +run the following command: + +```bash +./gradlew test connectedAndroidTest +``` \ No newline at end of file diff --git a/integration-tests/android/build.gradle.kts b/integration-tests/android/build.gradle.kts new file mode 100644 index 0000000..1c3e1f1 --- /dev/null +++ b/integration-tests/android/build.gradle.kts @@ -0,0 +1,77 @@ +buildscript { + repositories { + google() + mavenCentral() + } +} + +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.junit.android) + alias(libs.plugins.rust.android) +} + +android { + namespace = "rust.android.tests" + + compileSdk = libs.versions.compileSdk.get().toInt() + ndkVersion = libs.versions.ndk.get() + defaultConfig { + minSdk = libs.versions.minSdk.get().toInt() + targetSdk = libs.versions.targetSdk.get().toInt() + version = "1" + versionCode = 1 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.toString() + } + + externalNativeBuild { + cmake { + version = libs.versions.cmake.get() + } + } + + packaging { + resources { + merges += "META-INF/LICENSE.md" + merges += "META-INF/LICENSE-notice.md" + } + } +} + +kotlin { + jvmToolchain(17) +} + +tasks.withType { + useJUnitPlatform() + systemProperties = mapOf( + "junit.jupiter.execution.parallel.enabled" to "true", + "junit.jupiter.execution.parallel.mode.default " to "concurrent", + ) +} + +dependencies { + implementation(libs.appcompat) + implementation(libs.annotations) + testRuntimeOnly(libs.junit.engine) + testImplementation(libs.bundles.test) + androidTestImplementation(libs.bundles.test.instrumentation) +} + +androidRust { + module("rustls-native-certs-android-tests") { + path = file("./") + targets = listOf("arm", "arm64", "x86", "x86_64") + } +} \ No newline at end of file diff --git a/integration-tests/android/gradle.properties b/integration-tests/android/gradle.properties new file mode 100644 index 0000000..9eb0657 --- /dev/null +++ b/integration-tests/android/gradle.properties @@ -0,0 +1,8 @@ +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +org.gradle.daemon=true +org.gradle.parallel=true +org.gradle.caching=true + +kotlin.code.style=official +android.useAndroidX=true +android.nonTransitiveRClass=true \ No newline at end of file diff --git a/integration-tests/android/gradle/wrapper/gradle-wrapper.jar b/integration-tests/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e644113 Binary files /dev/null and b/integration-tests/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/integration-tests/android/gradle/wrapper/gradle-wrapper.properties b/integration-tests/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..66cd5a0 --- /dev/null +++ b/integration-tests/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-all.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/integration-tests/android/gradlew b/integration-tests/android/gradlew new file mode 100755 index 0000000..1aa94a4 --- /dev/null +++ b/integration-tests/android/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/integration-tests/android/gradlew.bat b/integration-tests/android/gradlew.bat new file mode 100644 index 0000000..25da30d --- /dev/null +++ b/integration-tests/android/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/integration-tests/android/libs.versions.toml b/integration-tests/android/libs.versions.toml new file mode 100644 index 0000000..a567a96 --- /dev/null +++ b/integration-tests/android/libs.versions.toml @@ -0,0 +1,38 @@ +[versions] +compileSdk = "35" +minSdk = "24" +targetSdk = "35" +ndk = "28.0.12433566" +cmake = "3.30.5" + +android-gradle-plugin = "8.6.0-alpha07" +kotlin = "2.0.20" +appcompat = "1.7.0" +annotation = "1.8.2" +rust-android = "0.3.2" +junit = "5.11.2" +junit-android = "1.11.0.0" +espresso-core = "3.6.1" +test-core = "1.6.1" +test-runner = "1.6.2" +androidx-test-ext-junit = "1.2.1" + +[libraries] +appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } +annotations = { group = "androidx.annotation", name = "annotation", version.ref = "annotation" } +junit = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" } +junit-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" } +test-core = { module = "androidx.test:core", version.ref = "test-core" } +test-runner = { module = "androidx.test:runner", version.ref = "test-runner" } +androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" } +espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" } + +[bundles] +test = ["junit"] +test-instrumentation = ["junit", "test-core", "test-runner", "androidx-test-ext-junit", "espresso-core"] + +[plugins] +android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +junit-android = { id = "de.mannodermaus.android-junit5", version.ref = "junit-android" } +rust-android = { id = "io.github.MatrixDev.android-rust", version.ref = "rust-android" } diff --git a/integration-tests/android/settings.gradle.kts b/integration-tests/android/settings.gradle.kts new file mode 100644 index 0000000..64635d0 --- /dev/null +++ b/integration-tests/android/settings.gradle.kts @@ -0,0 +1,28 @@ +rootProject.name = "rust-native-certs-android-tests" + +pluginManagement { + repositories { + gradlePluginPortal() + google() + mavenCentral() + } +} + +@Suppress("UnstableApiUsage") +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + + repositories { + gradlePluginPortal() + google() + mavenCentral() + } + + versionCatalogs { + create("libs") { + from(files("./libs.versions.toml")) + } + } +} + +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") \ No newline at end of file diff --git a/integration-tests/android/src/androidTest/kotlin/rustls/android/tests/CompareMozilla.kt b/integration-tests/android/src/androidTest/kotlin/rustls/android/tests/CompareMozilla.kt new file mode 100644 index 0000000..a5c1fef --- /dev/null +++ b/integration-tests/android/src/androidTest/kotlin/rustls/android/tests/CompareMozilla.kt @@ -0,0 +1,19 @@ +package rustls.android.tests + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.runner.RunWith +import org.junit.Test + +@RunWith(AndroidJUnit4::class) +class CompareMozilla { + @Test external fun test_does_not_have_many_roots_unknown_by_mozilla() + @Test external fun test_contains_most_roots_known_by_mozilla() + @Test external fun util_list_certs() + + companion object { + const val LIB_NAME = "rustls_native_certs_android_tests" + init { + System.loadLibrary(LIB_NAME) + } + } +} \ No newline at end of file diff --git a/integration-tests/android/src/androidTest/kotlin/rustls/android/tests/SmokeTests.kt b/integration-tests/android/src/androidTest/kotlin/rustls/android/tests/SmokeTests.kt new file mode 100644 index 0000000..224be29 --- /dev/null +++ b/integration-tests/android/src/androidTest/kotlin/rustls/android/tests/SmokeTests.kt @@ -0,0 +1,40 @@ +package rustls.android.tests + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Ignore +import org.junit.runner.RunWith +import org.junit.Test + +@RunWith(AndroidJUnit4::class) +class SmokeTests { + @Test fun google() = check_site("google.com") + @Test fun amazon() = check_site("amazon.com") + @Test fun facebook() = check_site("facebook.com") + @Test @Ignore("flaky. server reseet connection") fun netflix() = check_site("netflix.com") + @Test fun ebay() = check_site("ebay.com") + @Test fun apple() = check_site("apple.com") + @Test fun microsoft() = check_site("microsoft.com") + @Test(expected = java.lang.Error::class) // AlertReceived(HandshakeFailure) + fun badssl() = check_site("badssl.com") + @Test(expected = java.lang.Error::class) // AlertReceived(HandshakeFailure) + fun badssl_expired() = check_site("expired.badssl.com") + @Test(expected = java.lang.Error::class) // AlertReceived(HandshakeFailure) + fun badssl_wrong_host() = check_site("wrong.host.badssl.com") + @Test(expected = java.lang.Error::class) // AlertReceived(HandshakeFailure) + fun badssl_self_signed() = check_site("self-signed.badssl.com") + @Test(expected = java.lang.Error::class) // AlertReceived(HandshakeFailure) + fun badssl_untrusted_root() = check_site("untrusted-root.badssl.com") + @Test(expected = java.lang.Error::class) // AlertReceived(HandshakeFailure) + fun badssl_revoked() = check_site("revoked.badssl.com") + @Test(expected = java.lang.Error::class) // AlertReceived(HandshakeFailure) + fun badssl_pinning_test() = check_site("pinning-test.badssl.com") + + external fun check_site(site: String) + + companion object { + const val LIB_NAME = "rustls_native_certs_android_tests" + init { + System.loadLibrary(LIB_NAME) + } + } +} \ No newline at end of file diff --git a/integration-tests/android/src/main/AndroidManifest.xml b/integration-tests/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..6fb8c41 --- /dev/null +++ b/integration-tests/android/src/main/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/integration-tests/android/src/main/kotlin/rustls/android/tests/MainActivity.kt b/integration-tests/android/src/main/kotlin/rustls/android/tests/MainActivity.kt new file mode 100644 index 0000000..5f8db17 --- /dev/null +++ b/integration-tests/android/src/main/kotlin/rustls/android/tests/MainActivity.kt @@ -0,0 +1,11 @@ +package rustls.android.tests + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity + +class MainActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + } +} + diff --git a/integration-tests/android/src/main/res/values/styles.xml b/integration-tests/android/src/main/res/values/styles.xml new file mode 100644 index 0000000..515d186 --- /dev/null +++ b/integration-tests/android/src/main/res/values/styles.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/integration-tests/android/src/main/rust/lib.rs b/integration-tests/android/src/main/rust/lib.rs new file mode 100644 index 0000000..49f0347 --- /dev/null +++ b/integration-tests/android/src/main/rust/lib.rs @@ -0,0 +1,2 @@ +#[cfg(target_os = "android")] +pub mod tests; diff --git a/integration-tests/android/src/main/rust/tests.rs b/integration-tests/android/src/main/rust/tests.rs new file mode 100644 index 0000000..bbe51d5 --- /dev/null +++ b/integration-tests/android/src/main/rust/tests.rs @@ -0,0 +1,315 @@ +#[allow(non_snake_case)] +pub mod android { + extern crate android_log_sys as android_log; + extern crate jni; + extern crate rustls; + extern crate rustls_native_certs; + extern crate untrusted; + extern crate webpki; + + use jni::objects::{JObject, JString}; + use jni::JNIEnv; + use ring::io::der; + use rustls::pki_types::Der; + use std::collections::HashMap; + use std::ffi::CString; + use std::io::{ErrorKind, Read, Write}; + use std::net::TcpStream; + use std::sync::Arc; + use webpki::anchor_from_trusted_cert; + + #[no_mangle] + pub unsafe extern "C" fn Java_rustls_android_tests_CompareMozilla_test_1does_1not_1have_1many_1roots_1unknown_1by_1mozilla< + 'local, + >( + mut env: JNIEnv<'local>, + _this: JObject<'local>, + ) -> JObject<'local> { + let native = rustls_native_certs::load_native_certs(); + let mozilla = webpki_roots::TLS_SERVER_ROOTS + .iter() + .map(|ta| (ta.subject_public_key_info.as_ref(), ta)) + .collect::>(); + + let mut missing_in_moz_roots = 0; + + for cert in &native.certs { + let cert = anchor_from_trusted_cert(cert).unwrap(); + if let Some(moz) = mozilla.get(cert.subject_public_key_info.as_ref()) { + if assert( + &mut env, + *cert.subject == *moz.subject, + Some("subjects differ for public key"), + ) { + return JObject::null(); + } + } else { + log( + TAG, + format!( + "Native anchor {:?} is missing from mozilla set", + stringify_x500name(&cert.subject) + ) + .as_str(), + ); + missing_in_moz_roots += 1; + } + } + + #[cfg(target_os = "android")] + let threshold = 0.3; // no more than 30% extra roots; + + let diff = (missing_in_moz_roots as f64) / (mozilla.len() as f64); + log(TAG, format!("mozilla: {:?}", mozilla.len()).as_str()); + log(TAG, format!("native: {:?}", native.certs.len()).as_str()); + log( + TAG, + format!( + "{:?} anchors present in native set but not mozilla ({}%)", + missing_in_moz_roots, + diff * 100. + ) + .as_str(), + ); + + assert(&mut env, diff < threshold, Some("too many missing roots")); + + return JObject::null(); + } + + #[no_mangle] + pub unsafe extern "C" fn Java_rustls_android_tests_CompareMozilla_test_1contains_1most_1roots_1known_1by_1mozilla< + 'local, + >( + mut env: JNIEnv<'local>, + _this: JObject<'local>, + ) -> JObject<'local> { + let native = rustls_native_certs::load_native_certs(); + + let mut native_map = HashMap::new(); + for anchor in &native.certs { + let cert = anchor_from_trusted_cert(anchor).unwrap(); + let spki = cert.subject_public_key_info.as_ref(); + native_map.insert(spki.to_owned(), anchor); + } + + let mut missing_in_native_roots = 0; + let mozilla = webpki_roots::TLS_SERVER_ROOTS; + for cert in mozilla { + if !native_map.contains_key(cert.subject_public_key_info.as_ref()) { + log( + TAG, + format!( + "Mozilla anchor {:?} is missing from native set", + stringify_x500name(&cert.subject) + ) + .as_str(), + ); + missing_in_native_roots += 1; + } + } + + #[cfg(target_os = "android")] + let threshold = 0.3; // no more than 30% extra roots; + + let diff = (missing_in_native_roots as f64) / (mozilla.len() as f64); + log(TAG, format!("mozilla: {:?}", mozilla.len()).as_str()); + log(TAG, format!("native: {:?}", native.certs.len()).as_str()); + log( + TAG, + format!( + "{:?} anchors present in mozilla set but not native ({}%)", + missing_in_native_roots, + diff * 100. + ) + .as_str(), + ); + + assert(&mut env, diff < threshold, Some("too many missing roots")); + + return JObject::null(); + } + + #[no_mangle] + pub unsafe extern "C" fn Java_rustls_android_tests_CompareMozilla_util_1list_1certs<'local>( + mut _env: JNIEnv<'local>, + _this: JObject<'local>, + ) -> JObject<'local> { + let native = rustls_native_certs::load_native_certs(); + for (i, cert) in native.certs.iter().enumerate() { + let cert = anchor_from_trusted_cert(cert).unwrap(); + log( + TAG, + format!("cert[{i}] = {}", stringify_x500name(&cert.subject)).as_str(), + ); + } + return JObject::null(); + } + + #[no_mangle] + pub unsafe extern "C" fn Java_rustls_android_tests_SmokeTests_check_1site<'local>( + mut env: JNIEnv<'local>, + _this: JString<'local>, + domain: JString<'local>, + ) -> JObject<'local> { + let domain_string = env.get_string(&domain).unwrap(); + let domain_str = domain_string.to_str().unwrap(); + let _ = check_site(&mut env, domain_str); + return JObject::null(); + } + + fn stringify_x500name(subject: &Der<'_>) -> String { + let mut parts = vec![]; + let mut reader = untrusted::Reader::new(subject.as_ref().into()); + + while !reader.at_end() { + let (tag, contents) = der::read_tag_and_get_value(&mut reader).unwrap(); + assert!(tag == 0x31); // sequence, constructed, context=1 + + let mut inner = untrusted::Reader::new(contents); + let pair = der::expect_tag_and_get_value(&mut inner, der::Tag::Sequence).unwrap(); + + let mut pair = untrusted::Reader::new(pair); + let oid = der::expect_tag_and_get_value(&mut pair, der::Tag::OID).unwrap(); + let (valuety, value) = der::read_tag_and_get_value(&mut pair).unwrap(); + + let name = match oid.as_slice_less_safe() { + [0x55, 0x04, 0x03] => "CN", + [0x55, 0x04, 0x05] => "serialNumber", + [0x55, 0x04, 0x06] => "C", + [0x55, 0x04, 0x07] => "L", + [0x55, 0x04, 0x08] => "ST", + [0x55, 0x04, 0x09] => "STREET", + [0x55, 0x04, 0x0a] => "O", + [0x55, 0x04, 0x0b] => "OU", + [0x55, 0x04, 0x11] => "postalCode", + [0x55, 0x04, 0x61] => "organizationIdentifier", + [0x09, 0x92, 0x26, 0x89, 0x93, 0xf2, 0x2c, 0x64, 0x01, 0x19] => "domainComponent", + [0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x01] => "emailAddress", + _ => panic!("unhandled x500 attr {:?}", oid), + }; + + let str_value = match valuety { + // PrintableString, UTF8String, TeletexString or IA5String + 0x0c | 0x13 | 0x14 | 0x16 => { + std::str::from_utf8(value.as_slice_less_safe()).unwrap() + } + _ => panic!("unhandled x500 value type {:?}", valuety), + }; + + parts.push(format!("{}={}", name, str_value)); + } + + parts.join(", ") + } + + fn check_site(env: &mut JNIEnv, domain: &str) -> Result<(), ()> { + check_site_with_roots( + env, + domain, + rustls_native_certs::load_native_certs().unwrap(), + ) + } + + fn check_site_with_roots( + env: &mut JNIEnv, + domain: &str, + root_certs: Vec>, + ) -> Result<(), ()> { + let mut roots = rustls::RootCertStore::empty(); + roots.add_parsable_certificates(root_certs); + + let mut config = rustls::ClientConfig::builder() + .with_root_certificates(roots) + .with_no_client_auth(); + + let mut alpn = Vec::::new(); + alpn.push("http/1.1".to_string()); + config.alpn_protocols = alpn + .iter() + .map(|proto| proto.as_bytes().to_vec()) + .collect(); + + let mut conn = rustls::ClientConnection::new( + Arc::new(config), + rustls::pki_types::ServerName::try_from(domain) + .unwrap() + .to_owned(), + ) + .unwrap(); + let mut sock = TcpStream::connect(format!("{}:443", domain)).unwrap(); + let mut tls = rustls::Stream::new(&mut conn, &mut sock); + let request = format!( + "GET / HTTP/1.1\r\n\ + Host: {}\r\n\ + Connection: close\r\n\ + Accept-Encoding: identity\r\n\ + \r\n", + domain + ); + + log(TAG, "REQUEST >>>>>>>>>>>>>"); + log(TAG, request.as_str()); + let result = tls.write_all(request.as_bytes()); + match result { + Ok(()) => (), + Err(e) if e.kind() == ErrorKind::InvalidData => { + panic(env, format!("{:?}", e).as_str()); + return Err(()); + } // TLS error + Err(e) => { + panic(env, format!("{:?}", e).as_str()); + return Err(()); + } + } + + let mut plaintext = [0u8; 64]; + let len = tls.read(&mut plaintext); //.unwrap(); + log(TAG, format!("read {:?}", len).as_str()); + // log(TAG, "RESPONSE <<<<<<<<<<<<<"); + // // log(TAG, &String::from_utf8_lossy(&plaintext[..len])); + // log(TAG, format!("{:?}", &plaintext[..len]).as_str()); + // assert(env, plaintext[..len].starts_with(b"HTTP/1.1 "), None); // or whatever + Ok(()) + } + + const DEFAULT_PRIO: android_log::c_int = + android_log::LogPriority::DEFAULT as android_log::c_int; + const TAG: &str = "RUSTLS_NATIVE_CERTS"; + fn log(tag: &str, msg: &str) { + let c_prio = android_log::LogPriority::DEBUG as android_log::c_int; + let c_tag = CString::new(tag).unwrap().into_raw(); + unsafe { + if android_log::__android_log_is_loggable(c_prio, c_tag, DEFAULT_PRIO) != 0 { + let c_msg = CString::new(msg).unwrap().into_raw(); + android_log::__android_log_print(c_prio, c_tag, c_msg); + } + }; + } + + pub fn assert<'local>(env: &mut JNIEnv<'local>, test: bool, msg: Option<&str>) -> bool { + if env.exception_check().unwrap() { + env.exception_clear().unwrap(); + } + if !test { + let clazz = env + .find_class("java/lang/AssertionError") + .unwrap(); + match msg { + Some(msg) => env.throw_new(clazz, msg).unwrap(), + None => env.throw_new(clazz, "").unwrap(), + } + } + return !test; + } + + pub fn panic<'local>(env: &mut JNIEnv<'local>, msg: &str) { + if env.exception_check().unwrap() { + env.exception_clear().unwrap(); + } + let clazz = env + .find_class("java/lang/Error") + .unwrap(); + env.throw_new(clazz, msg).unwrap(); + } +} diff --git a/src/android.rs b/src/android.rs new file mode 100644 index 0000000..8e86cf1 --- /dev/null +++ b/src/android.rs @@ -0,0 +1,287 @@ +extern crate jni; + +use crate::CertificateResult; +use jni::errors::Error; +use jni::objects::{JByteArray, JObject, JString, JValue}; +use jni::sys::JavaVM as JavaVMSys; +use jni::sys::{jint, jsize, JNI_OK}; +use jni::{AttachGuard, JavaVM}; +use pki_types::CertificateDer; +use std::fmt::{Debug, Display, Formatter}; + +pub type JniGetCreatedJavaVms = unsafe extern "system" fn( + vm_buf: *mut *mut jni::sys::JavaVM, + buf_len: jsize, + n_vms: *mut jsize, +) -> jint; + +pub fn load_native_certs() -> CertificateResult { + let mut result = CertificateResult::default(); + + // Get a JVM + let jvm = match get_jvm() { + Ok(jvm) => jvm, + Err(err) => { + result.errors.push(err); + return result; + } + }; + + // Get a JVM environment + let mut env = match jvm.attach_current_thread() { + Ok(env) => env, + Err(err) => { + result.errors.push(From::from(err)); + return result; + } + }; + + // Load the Android keystore + let mut keystore = match get_key_store(&mut env) { + Ok(keystore) => keystore, + Err(err) => { + result.errors.push(err); + return result; + } + }; + + // Extract any certificates in the keystore + if let Err(err) = extract_certificates(&mut env, &mut keystore, &mut result) { + result.errors.push(err); + return result; + }; + + // Clear any JNI exception as any error is returned via CertificateResult + if env.exception_check().is_ok() { + let _ = env.exception_clear(); + } + + result +} + +fn get_jvm() -> Result { + let library = libloading::os::unix::Library::this(); + let fn_get_created_java_vms: JniGetCreatedJavaVms = + unsafe { *library.get(b"JNI_GetCreatedJavaVMs")? }; + let mut created_java_vms: [*mut JavaVMSys; 1] = [std::ptr::null_mut() as *mut JavaVMSys]; + let mut vms_read: i32 = 0; + match unsafe { fn_get_created_java_vms(created_java_vms.as_mut_ptr(), 1, &mut vms_read) } { + JNI_OK => {} + x => { + let context = format!("Failed to obtain JVM reference [code: {}]", x).to_string(); + Err(FFIError::new(context))? + } + }; + let jvm_ptr = match created_java_vms.first() { + Some(ptr) => *ptr, + None => Err(FFIError::new("No JVM created".to_string()))?, + }; + Ok(unsafe { JavaVM::from_raw(jvm_ptr) }?) +} + +fn get_key_store<'a>(env: &mut AttachGuard<'a>) -> Result, crate::Error> { + let clazz = env.find_class("java/security/KeyStore")?; + let keystore_name = &JObject::from(env.new_string("AndroidCAStore")?); + let args = &[JValue::Object(keystore_name)]; + let keystore = env + .call_static_method( + clazz, + "getInstance", + "(Ljava/lang/String;)Ljava/security/KeyStore;", + args, + )? + .l()?; + + let null = JObject::null(); + let args = &[JValue::Object(&null)]; + let _ = &env + .call_method( + &keystore, + "load", + "(Ljava/security/KeyStore$LoadStoreParameter;)V", + args, + )? + .v()?; + + Ok(keystore) +} + +fn extract_certificates<'a>( + env: &mut AttachGuard<'a>, + keystore: &mut JObject, + result: &mut CertificateResult, +) -> Result<(), crate::Error> { + // Enumerate each certificate alias in the keystore + let enumeration = &env + .call_method(&keystore, "aliases", "()Ljava/util/Enumeration;", &[])? + .l()?; + loop { + // Check if there are more aliases + if !env + .call_method(enumeration, "hasMoreElements", "()Z", &[])? + .z()? + { + break; + } + + // Get the certificate alias + let alias = env + .call_method(enumeration, "nextElement", "()Ljava/lang/Object;", &[])? + .l()?; + + // Read the certificate + read_certificate(env, keystore, alias, result); + } + Ok(()) +} + +fn read_certificate<'a>( + env: &mut AttachGuard<'a>, + keystore: &mut JObject, + alias: JObject, + result: &mut CertificateResult, +) { + let alias_jstring = JString::from(alias); + let alias_str = match env.get_string(&alias_jstring) { + Ok(value) => value + .to_str() + .unwrap_or_else(|_| "unknown") + .to_string(), + Err(_) => "unknown".to_string(), + }; + let args = &[JValue::Object(&alias_jstring)]; + let ret = env.call_method( + keystore, + "getCertificate", + "(Ljava/lang/String;)Ljava/security/cert/Certificate;", + args, + ); + let certificate = match ret { + Ok(value) => match value.l() { + Ok(cert) => cert, + Err(err) => { + let context = format!("Failed to read certificate. Alias: {}", alias_str); + result + .errors + .push(From::from(FFIError::new_with_source(context, err))); + return; + } + }, + Err(err) => { + let context = format!("Failed to read certificate. Alias: {}", alias_str); + result + .errors + .push(From::from(FFIError::new_with_source(context, err))); + return; + } + }; + + // Get the der encoded bytes + let ret = env.call_method(certificate, "getEncoded", "()[B", &[]); + let der_object = match ret { + Ok(value) => match value.l() { + Ok(der_object) => der_object, + Err(err) => { + let context = format!("Failed to decode certificate. Alias: {}", alias_str); + result + .errors + .push(From::from(FFIError::new_with_source(context, err))); + return; + } + }, + Err(err) => { + let context = format!("Failed to decode certificate. Alias: {}", alias_str); + result + .errors + .push(From::from(FFIError::new_with_source(context, err))); + return; + } + }; + + // Get the amount of bytes + let der_byte_array = JByteArray::from(der_object); + let length = match env.get_array_length(&der_byte_array) { + Ok(size) => size as usize, + Err(err) => { + let context = format!("Failed to decode certificate. Alias: {}", alias_str); + result + .errors + .push(From::from(FFIError::new_with_source(context, err))); + return; + } + }; + + // Read from JNI ByteArray to rust Vec + let mut der_encoded_data: Vec = vec![0; length]; + match env.get_byte_array_region(der_byte_array, 0, &mut der_encoded_data) { + Ok(_) => {} + Err(err) => { + let context = format!("Failed to decode certificate. Alias: {}", alias_str); + result + .errors + .push(From::from(FFIError::new_with_source(context, err))); + return; + } + }; + + // Transform from i8 to u8 and store the certificate + let certificate_der = CertificateDer::from( + unsafe { &*(der_encoded_data.as_slice() as *const _ as *const [u8]) }.to_owned(), + ); + result.certs.push(certificate_der); +} + +#[derive(Debug)] +struct FFIError { + context: String, + source: Option, +} +impl FFIError { + fn new_with_source(context: String, source: jni::errors::Error) -> Self { + FFIError { + context, + source: Some(source), + } + } + + fn new(context: String) -> Self { + FFIError { + context, + source: None, + } + } +} +impl std::error::Error for FFIError {} +impl Display for FFIError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.context) + } +} + +impl From for crate::Error { + fn from(value: FFIError) -> Self { + crate::Error { + context: "Foreign Function operation failed", + kind: match value.source { + Some(error) => crate::ErrorKind::Os(Box::new(error)), + None => crate::ErrorKind::Os(Box::new(value)), + }, + } + } +} +impl From for crate::Error { + fn from(value: libloading::Error) -> Self { + crate::Error { + context: "Failed to load library", + kind: crate::ErrorKind::Os(Box::new(value)), + } + } +} +impl From for crate::Error { + fn from(value: Error) -> Self { + crate::Error { + context: "Jni operation failed", + kind: crate::ErrorKind::Os(Box::new(value)), + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 14f4cb1..99dafa4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -30,9 +30,9 @@ use std::{env, fmt}; use pki_types::CertificateDer; -#[cfg(all(unix, not(target_os = "macos")))] +#[cfg(all(unix, not(target_os = "macos"), not(target_os = "android")))] mod unix; -#[cfg(all(unix, not(target_os = "macos")))] +#[cfg(all(unix, not(target_os = "macos"), not(target_os = "android")))] use unix as platform; #[cfg(windows)] @@ -45,6 +45,11 @@ mod macos; #[cfg(target_os = "macos")] use macos as platform; +#[cfg(target_os = "android")] +mod android; +#[cfg(target_os = "android")] +use android as platform; + /// Load root certificates found in the platform's native certificate store. /// /// ## Environment Variables