-
Notifications
You must be signed in to change notification settings - Fork 9.3k
Add named groups (supported_groups) to ConnectionSpec for post-quantum key exchange #9517
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 7 commits
063c450
ca45786
091508a
a6a3224
6bd89a1
0011d82
75de41e
1f7cb6d
5477957
ca9b3ba
2487f24
e7544f3
8c12bc9
ce5b97a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,139 @@ | ||
| # Post-quantum TLS test for Android: a PQC-only server runs as a Docker container on the runner host | ||
| # and the emulator reaches it at 10.0.2.2. Opt-in (heavy: emulator + Docker) via label or manual run. | ||
| # | ||
| # The emulator setup mirrors the `android` job in build.yml (extracted from testing_ech), including the | ||
| # experimental API 37 (16 KB page-size) entry. | ||
| name: android-containers | ||
|
|
||
| on: | ||
| workflow_dispatch: | ||
| pull_request: | ||
| types: [labeled, synchronize] | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| android-pqc: | ||
| # Only run on manual dispatch or when the PR carries the "android-containers" label. | ||
| if: > | ||
| github.event_name == 'workflow_dispatch' | ||
| || contains(github.event.pull_request.labels.*.name, 'android-containers') | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 30 | ||
| continue-on-error: ${{ matrix.experimental || false }} | ||
|
|
||
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| include: | ||
| - api-level: 34 | ||
| arch: x86_64 | ||
| target: default | ||
| # API 37 only ships 16 KB page-size images; playstore_ps16k resolves to | ||
| # google_apis_playstore_ps16k. Quoted so YAML keeps the ".0". | ||
| - api-level: "37.0" | ||
| arch: x86_64 | ||
| target: playstore_ps16k | ||
| memory: 4096 | ||
| gpu: -gpu auto | ||
| experimental: true | ||
|
|
||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v7 | ||
|
|
||
| - name: Configure JDK | ||
| uses: actions/setup-java@v5 | ||
| with: | ||
| distribution: 'temurin' | ||
| java-version: 21 | ||
|
|
||
| - name: Enable KVM group perms | ||
| 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: Setup Gradle | ||
| uses: gradle/actions/setup-gradle@v5 | ||
Check failureCode scanning / zizmor unpinned action reference Error
unpinned action reference
Check failureCode scanning / Semgrep OSS Insecure GitHub Actions: Third-Party Action Not Pinned to Commit SHA Error
Insecure GitHub Actions: Third-Party Action Not Pinned to Commit SHA
|
||
|
github-advanced-security[bot] marked this conversation as resolved.
Fixed
|
||
|
|
||
| - name: Warm the Gradle cache | ||
| run: ./gradlew :android-test:assembleDebugAndroidTest | ||
|
|
||
| # OpenSSL >= 3.5 ships native ML-KEM; restrict the server to the PQC hybrid group so a | ||
| # successful handshake proves the group was negotiated. Maps container 4433 -> host 8443. | ||
| - name: Start post-quantum server | ||
| run: | | ||
| docker run -d --name pqc-server -p 8443:4433 alpine:3.22 sh -c ' | ||
| apk add --no-cache openssl && | ||
| openssl req -x509 -newkey rsa:2048 -nodes -days 1 -subj /CN=localhost \ | ||
| -keyout /key.pem -out /cert.pem && | ||
| openssl s_server -accept 4433 -cert /cert.pem -key /key.pem \ | ||
| -groups X25519MLKEM768 -tls1_3 -www | ||
| ' | ||
| # Wait for the TLS port to accept connections. | ||
| for i in $(seq 1 30); do | ||
| if openssl s_client -connect 127.0.0.1:8443 -groups X25519MLKEM768 </dev/null 2>/dev/null | grep -q "BEGIN CERTIFICATE"; then | ||
| echo "PQC server ready" && break | ||
| fi | ||
| sleep 2 | ||
| done | ||
|
|
||
| - name: AVD System Image Cache | ||
| uses: actions/cache@v6 | ||
| id: avd-cache | ||
| with: | ||
| key: avd-${{ runner.os }}-${{ matrix.api-level }}-${{ matrix.target }}-${{ matrix.arch }} | ||
| path: | | ||
| ~/.android/avd/* | ||
| ~/.android/adb* | ||
| ${{ env.ANDROID_HOME }}/system-images/android-${{ matrix.api-level }} | ||
|
|
||
| - name: Create AVD and generate snapshot for caching | ||
| if: steps.avd-cache.outputs.cache-hit != 'true' | ||
| uses: reactivecircus/android-emulator-runner@v2 | ||
Check failureCode scanning / zizmor unpinned action reference Error
unpinned action reference
Check failureCode scanning / Semgrep OSS Insecure GitHub Actions: Third-Party Action Not Pinned to Commit SHA Error
Insecure GitHub Actions: Third-Party Action Not Pinned to Commit SHA
|
||
|
github-advanced-security[bot] marked this conversation as resolved.
Fixed
github-advanced-security[bot] marked this conversation as resolved.
Fixed
|
||
| with: | ||
| api-level: ${{ matrix.api-level }} | ||
| force-avd-creation: false | ||
| target: ${{ matrix.target }} | ||
| arch: ${{ matrix.arch }} | ||
| emulator-boot-timeout: 120 | ||
| emulator-options: > | ||
| -no-window | ||
| ${{ matrix.gpu || '-gpu swiftshader_indirect' }} | ||
| -noaudio | ||
| -no-boot-anim | ||
| -camera-back none | ||
| -memory ${{ matrix.memory || '2048' }} | ||
| -feature GLDirectMem,HasSharedSlotsHostMemoryAllocator | ||
| disable-animations: true | ||
| script: echo "Generated AVD snapshot for caching." | ||
|
|
||
| # The emulator reaches the host container at 10.0.2.2; pass the endpoint to the test and run | ||
| # only the PQC test class. Boot options must match the snapshot step above. | ||
| - name: Run post-quantum Android test | ||
| uses: reactivecircus/android-emulator-runner@v2 | ||
Check failureCode scanning / zizmor unpinned action reference Error
unpinned action reference
Check failureCode scanning / Semgrep OSS Insecure GitHub Actions: Third-Party Action Not Pinned to Commit SHA Error
Insecure GitHub Actions: Third-Party Action Not Pinned to Commit SHA
|
||
|
github-advanced-security[bot] marked this conversation as resolved.
Fixed
github-advanced-security[bot] marked this conversation as resolved.
Fixed
|
||
| with: | ||
| api-level: ${{ matrix.api-level }} | ||
| target: ${{ matrix.target }} | ||
| arch: ${{ matrix.arch }} | ||
| emulator-boot-timeout: 120 | ||
| emulator-options: > | ||
| -no-window | ||
| ${{ matrix.gpu || '-gpu swiftshader_indirect' }} | ||
| -noaudio | ||
| -no-boot-anim | ||
| -camera-back none | ||
| -memory ${{ matrix.memory || '2048' }} | ||
| -feature GLDirectMem,HasSharedSlotsHostMemoryAllocator | ||
| script: > | ||
| ./gradlew -PandroidBuild=true :android-test:connectedCheck | ||
| -Pandroid.testInstrumentationRunnerArguments.class=okhttp.android.test.PostQuantumAndroidTest | ||
| -Pandroid.testInstrumentationRunnerArguments.pqcServerUrl=https://10.0.2.2:8443/ | ||
| env: | ||
| API_LEVEL: ${{ matrix.api-level }} | ||
|
|
||
| - name: Stop post-quantum server | ||
| if: always() | ||
| run: docker rm -f pqc-server || true | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,129 @@ | ||
| /* | ||
| * Copyright (C) 2026 Square, Inc. | ||
| * | ||
| * 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 | ||
| * | ||
| * http://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. | ||
| */ | ||
| package okhttp.android.test | ||
|
|
||
| import android.content.Context | ||
| import androidx.test.core.app.ApplicationProvider | ||
| import androidx.test.platform.app.InstrumentationRegistry | ||
| import assertk.assertThat | ||
| import assertk.assertions.contains | ||
| import assertk.assertions.isEqualTo | ||
| import java.security.SecureRandom | ||
| import java.security.Security | ||
| import java.security.cert.X509Certificate | ||
| import javax.net.ssl.SSLContext | ||
| import javax.net.ssl.X509TrustManager | ||
| import okhttp3.ConnectionSpec | ||
| import okhttp3.HttpUrl.Companion.toHttpUrl | ||
| import okhttp3.NamedGroup | ||
| import okhttp3.OkHttpClient | ||
| import okhttp3.Request | ||
| import okhttp3.internal.platform.PlatformRegistry | ||
| import org.conscrypt.Conscrypt | ||
| import org.junit.jupiter.api.AfterEach | ||
| import org.junit.jupiter.api.Assumptions.assumeTrue | ||
| import org.junit.jupiter.api.BeforeEach | ||
| import org.junit.jupiter.api.Test | ||
|
|
||
| /** | ||
| * Exercises post-quantum named-group key exchange on a real Android device/emulator. | ||
| * | ||
| * Unlike the JVM container tests, the server runs as a Docker container on the *host*: the emulator | ||
| * reaches it at `10.0.2.2`. The PQC-only server URL is passed in as the `pqcServerUrl` instrumentation | ||
| * argument (see the `android-containers` workflow); the test skips when it is absent, so it is inert | ||
| * in the regular `connectedCheck` matrix. | ||
| * | ||
| * Android's system Conscrypt has no PQC, so this installs the bundled `conscrypt-android` (2.6+), | ||
| * which does. A successful HTTPS response against a server restricted to [NamedGroup.X25519MLKEM768] | ||
| * proves the group was negotiated. | ||
| * | ||
| * Note: applying [ConnectionSpec.Builder.namedGroups] on Android needs the Android implementation of | ||
| * `applyNamedGroups` (tracked separately). Until that lands the spec setting is a no-op on device, so | ||
| * this test relies on the provider's default group preference; once it lands the configuration is | ||
| * honored explicitly. | ||
| */ | ||
| class PostQuantumAndroidTest { | ||
| private val serverUrl: String? = | ||
| InstrumentationRegistry.getArguments().getString("pqcServerUrl") | ||
|
|
||
| private lateinit var client: OkHttpClient | ||
|
|
||
| @BeforeEach | ||
| fun setup() { | ||
| PlatformRegistry.applicationContext = ApplicationProvider.getApplicationContext<Context>() | ||
| Security.insertProviderAt(Conscrypt.newProviderBuilder().build(), 1) | ||
| } | ||
|
|
||
| @AfterEach | ||
| fun tearDown() { | ||
| Security.removeProvider("Conscrypt") | ||
| if (::client.isInitialized) { | ||
| client.dispatcher.executorService.shutdown() | ||
| client.connectionPool.evictAll() | ||
| } | ||
| } | ||
|
|
||
| @Test | ||
| fun negotiatesPostQuantumGroup() { | ||
| assumeTrue(serverUrl != null, "pqcServerUrl not set; skipping (needs the PQC server container)") | ||
| assumeTrue(conscryptSupportsPostQuantum(), "bundled Conscrypt < 2.6 has no post-quantum key exchange") | ||
|
|
||
| // Conscrypt is installed above; cert validation is bypassed because this is about key exchange, | ||
| // not authentication (the container uses a throwaway self-signed certificate). | ||
| val sslContext = | ||
| SSLContext.getInstance("TLS").apply { | ||
| init(null, arrayOf(trustAllManager), SecureRandom()) | ||
| } | ||
|
|
||
| client = | ||
| OkHttpClient | ||
| .Builder() | ||
| .sslSocketFactory(sslContext.socketFactory, trustAllManager) | ||
| .hostnameVerifier { _, _ -> true } | ||
| .connectionSpecs( | ||
| listOf( | ||
| ConnectionSpec | ||
| .Builder(ConnectionSpec.RESTRICTED_TLS) | ||
| .namedGroups(NamedGroup.X25519MLKEM768) | ||
| .build(), | ||
| ), | ||
| ).build() | ||
|
|
||
| val response = client.newCall(Request(serverUrl!!.toHttpUrl())).execute() | ||
| response.use { | ||
| assertThat(response.code).isEqualTo(200) | ||
| // `openssl s_server -www` echoes the negotiated parameters in its HTML status page. | ||
| assertThat(response.body.string()).contains("Protocol") | ||
| } | ||
| } | ||
|
|
||
| companion object { | ||
| /** Conscrypt added X25519MLKEM768 and SSLParameters.setNamedGroups in 2.6. */ | ||
| private fun conscryptSupportsPostQuantum(): Boolean { | ||
| val version = Conscrypt.version() | ||
| return version.major() > 2 || (version.major() == 2 && version.minor() >= 6) | ||
| } | ||
|
|
||
| private val trustAllManager = | ||
| object : X509TrustManager { | ||
| override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {} | ||
|
|
||
| override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {} | ||
|
|
||
| override fun getAcceptedIssuers(): Array<X509Certificate> = arrayOf() | ||
| } | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.