1
0
mirror of https://github.com/square/okhttp.git synced 2025-11-23 06:42:24 +03:00

Refresh the OkHttp cipher suite process (#7393)

This commit is contained in:
Yuri Schimke
2022-09-18 08:38:01 +01:00
committed by GitHub
parent bb23c677c8
commit c56e8648ca
17 changed files with 596 additions and 0 deletions

View File

@@ -0,0 +1,26 @@
plugins {
kotlin("jvm")
application
id("com.google.devtools.ksp").version("1.6.21-1.0.6")
}
application {
mainClass.set("okhttp3.survey.RunSurveyKt")
}
dependencies {
implementation("com.squareup.okhttp3:okhttp:5.0.0-alpha.10")
implementation("com.squareup.okhttp3:okhttp-coroutines:5.0.0-alpha.10")
implementation(libs.conscrypt.openjdk)
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-moshi:2.9.0")
implementation(libs.squareup.moshi)
implementation(libs.squareup.moshi.kotlin)
ksp("com.squareup.moshi:moshi-kotlin-codegen:1.13.0")
}
tasks.compileJava {
options.isWarnings = false
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright (C) 2016 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 okhttp3.survey
import okhttp3.survey.types.Client
import okhttp3.survey.types.SuiteId
/**
* Organizes information on SSL cipher suite inclusion and precedence for this spreadsheet.
* https://docs.google.com/spreadsheets/d/1C3FdZSlCBq_-qrVwG1KDIzNIB3Hyg_rKAcgmSzOsHyQ/edit#gid=0
*/
class CipherSuiteSurvey(
val clients: List<Client>,
val ianaSuites: IanaSuites,
val orderBy: List<SuiteId>
) {
fun printGoogleSheet() {
print("name")
for (client in clients) {
print("\t")
print(client.nameAndVersion)
}
println()
val sortedSuites = ianaSuites.suites.sortedBy { ianaSuite ->
val index = orderBy.indexOfFirst { it.matches(ianaSuite) }
if (index == -1) Integer.MAX_VALUE else index
}
for (suiteId in sortedSuites) {
print(suiteId.name)
for (client in clients) {
print("\t")
val index = client.enabled.indexOfFirst { it.matches(suiteId) }
if (index != -1) {
print(index + 1)
} else if (client.supported.find { it.matches(suiteId) } != null) {
// Not currently supported, as since 3.9.x we filter to a list
// that is a subset of the platforms.
// The correct answer for developers who override ConnectionSpec,
// it would be the platform defaults, so look at Java and Android
// for the answers.
print("")
}
}
println()
}
}
}

View File

@@ -0,0 +1,81 @@
/*
* Copyright (C) 2016 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 okhttp3.survey
import java.io.IOException
import javax.net.ssl.SSLSocket
import javax.net.ssl.SSLSocketFactory
import okhttp3.ConnectionSpec
import okhttp3.OkHttp
import okhttp3.survey.types.Client
import okhttp3.survey.types.SuiteId
import okio.FileSystem
import okio.Path.Companion.toPath
import org.conscrypt.Conscrypt
fun currentOkHttp(ianaSuites: IanaSuites): Client {
val supportedSuites = buildList {
for (suite in ConnectionSpec.COMPATIBLE_TLS.cipherSuites!!) {
add(ianaSuites.fromJavaName(suite.javaName))
}
}
val enabledSuites = buildList {
for (suite in ConnectionSpec.MODERN_TLS.cipherSuites!!) {
add(ianaSuites.fromJavaName(suite.javaName))
}
}
return Client("OkHttp", OkHttp.VERSION, null, enabledSuites, supportedSuites)
}
fun historicOkHttp(version: String): Client {
val enabled = FileSystem.RESOURCES.read("okhttp_${version.replace(".", "_")}.txt".toPath()) {
this.readUtf8().lines().filter { it.isNotBlank() }.map {
SuiteId(null, it.trim())
}
}
return Client("OkHttp", version, null, enabled = enabled)
}
fun currentVm(ianaSuites: IanaSuites): Client {
return systemDefault(System.getProperty("java.vm.name"), System.getProperty("java.version"), ianaSuites)
}
fun conscrypt(ianaSuites: IanaSuites): Client {
val version = Conscrypt.version()
return systemDefault("Conscrypt", "" + version.major() + "." + version.minor(), ianaSuites)
}
fun systemDefault(name: String, version: String, ianaSuites: IanaSuites): Client {
return try {
val socketFactory = SSLSocketFactory.getDefault() as SSLSocketFactory
val sslSocket = socketFactory.createSocket() as SSLSocket
val supportedSuites = buildList {
for (suite in sslSocket.supportedCipherSuites) {
add(ianaSuites.fromJavaName(suite))
}
}
val enabledSuites = buildList {
for (suite in sslSocket.enabledCipherSuites) {
add(ianaSuites.fromJavaName(suite))
}
}
Client(name, version, null, enabledSuites, supportedSuites)
} catch (e: IOException) {
throw RuntimeException(e)
}
}

View File

@@ -0,0 +1,64 @@
/*
* Copyright (C) 2016 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 okhttp3.survey
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.executeAsync
import okhttp3.survey.types.SuiteId
import okio.ByteString
import okio.ByteString.Companion.decodeHex
import okio.IOException
/** Example: "0x00,0x08",TLS_RSA_EXPORT_WITH_DES40_CBC_SHA,Y,N,[RFC4346] */
val IANA_CSV_PATTERN = "\"0x(\\w\\w),0x(\\w\\w)\",(\\w+).*".toRegex()
fun parseIanaCsvRow(s: String): SuiteId? {
if (s.contains("Reserved") || s.contains("Unassigned")) return null
val matcher = IANA_CSV_PATTERN.matchEntire(s) ?: return null
val id: ByteString = (matcher.groupValues[1] + matcher.groupValues[2]).decodeHex()
return SuiteId(id, matcher.groupValues[3])
}
class IanaSuites(
val name: String, val suites: List<SuiteId>
) {
fun fromJavaName(javaName: String): SuiteId {
for (suiteId in suites) {
val alternateName = "TLS_" + javaName.substring(4)
if (suiteId.name == javaName || suiteId.name == alternateName) {
return suiteId
}
}
throw IllegalArgumentException("No such suite: $javaName")
}
}
suspend fun fetchIanaSuites(okHttpClient: OkHttpClient): IanaSuites {
val url = "https://www.iana.org/assignments/tls-parameters/tls-parameters-4.csv"
val call = okHttpClient.newCall(Request(url.toHttpUrl()))
val suites = call.executeAsync().use {
if (!it.isSuccessful) {
throw IOException("Failed ${it.code} ${it.message}")
}
it.body.string().lines()
}.mapNotNull { parseIanaCsvRow(it) }
return IanaSuites("current", suites)
}

View File

@@ -0,0 +1,108 @@
/*
* Copyright (C) 2018 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 okhttp3.survey
import java.security.Security
import okhttp3.Cache
import okhttp3.OkHttpClient
import okhttp3.survey.ssllabs.SslLabsScraper
import okhttp3.survey.types.Client
import okhttp3.survey.types.SuiteId
import okio.FileSystem
import okio.Path.Companion.toPath
import org.conscrypt.Conscrypt
suspend fun main() {
val includeConscrypt = false
val client = OkHttpClient.Builder()
.cache(Cache("build/okhttp_cache".toPath(), 100_000_000, FileSystem.SYSTEM))
.build()
val sslLabsScraper = SslLabsScraper(client)
try {
val ianaSuitesNew = fetchIanaSuites(client)
val sslLabsClients = sslLabsScraper.query()
val android5 = sslLabsClients.first { it.userAgent == "Android" && it.version == "5.0.0" }
val android9 = sslLabsClients.first { it.userAgent == "Android" && it.version == "9.0" }
val chrome33 = sslLabsClients.first { it.userAgent == "Chrome" && it.version == "33" }
val chrome57 = sslLabsClients.first { it.userAgent == "Chrome" && it.version == "57" }
val chrome80 = sslLabsClients.first { it.userAgent == "Chrome" && it.version == "80" }
val firefox34 = sslLabsClients.first { it.userAgent == "Firefox" && it.version == "34" }
val firefox53 = sslLabsClients.first { it.userAgent == "Firefox" && it.version == "53" }
val firefox73 = sslLabsClients.first { it.userAgent == "Firefox" && it.version == "73" }
val java7 = sslLabsClients.first { it.userAgent == "Java" && it.version == "7u25" }
val java12 = sslLabsClients.first { it.userAgent == "Java" && it.version == "12.0.1" }
val safari12iOS = sslLabsClients.first { it.userAgent == "Safari" && it.platform == "iOS 12.3.1" }
val safari12Osx = sslLabsClients.first { it.userAgent == "Safari" && it.platform == "MacOS 10.14.6 Beta" }
val okhttp = currentOkHttp(ianaSuitesNew)
val okHttp_4_10 = historicOkHttp("4.10")
val okHttp_3_14 = historicOkHttp("3.14")
val okHttp_3_13 = historicOkHttp("3.13")
val okHttp_3_11 = historicOkHttp("3.11")
val okHttp_3_9 = historicOkHttp("3.9")
val currentVm = currentVm(ianaSuitesNew)
val conscrypt = if (includeConscrypt) {
Security.addProvider(Conscrypt.newProvider())
conscrypt(ianaSuitesNew)
} else {
Client("Conscrypt", "Disabled", null, listOf())
}
val clients = listOf(
okhttp,
chrome80,
firefox73,
android9,
safari12iOS,
conscrypt,
currentVm,
okHttp_3_9,
okHttp_3_11,
okHttp_3_13,
okHttp_3_14,
okHttp_4_10,
android5,
java7,
java12,
firefox34,
firefox53,
chrome33,
chrome57,
safari12Osx
)
val orderBy = okhttp.enabled + chrome80.enabled + safari12Osx.enabled + rest(clients)
val survey = CipherSuiteSurvey(clients = clients, ianaSuites = ianaSuitesNew, orderBy = orderBy)
survey.printGoogleSheet()
} finally {
client.dispatcher.executorService.shutdown()
client.connectionPool.evictAll()
}
}
fun rest(clients: List<Client>): List<SuiteId> {
// combine all ciphers to get these near the top
return clients.flatMap { it.enabled }
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright (C) 2016 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 okhttp3.survey.ssllabs
import com.squareup.moshi.Moshi
import okhttp3.Call
import okhttp3.OkHttpClient
import okhttp3.survey.ssllabs.SslLabsService
import okhttp3.survey.types.Client
import okhttp3.survey.types.SuiteId
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
class SslLabsScraper(
private val callFactory: Call.Factory,
) {
private val moshi = Moshi.Builder().build()
private val moshiConverterFactory = MoshiConverterFactory.create(moshi)
private val retrofit = Retrofit.Builder()
.baseUrl(SslLabsService.BASE_URL)
.addConverterFactory(moshiConverterFactory)
.callFactory(callFactory)
.build()
private val api = retrofit.create(SslLabsService::class.java)
suspend fun query(): List<Client> {
return api.clients().map { userAgent ->
Client(userAgent.name, userAgent.version, userAgent.platform, enabled = userAgent.suiteNames.map { SuiteId(null, it) })
}
}
}
suspend fun main() {
val client = OkHttpClient()
val scraper = SslLabsScraper(client)
println(scraper.query())
client.connectionPool.evictAll()
client.dispatcher.executorService.shutdown()
}

View File

@@ -0,0 +1,12 @@
package okhttp3.survey.ssllabs
import retrofit2.http.GET
interface SslLabsService {
@GET("getClients")
suspend fun clients(): List<UserAgentCapabilities>
companion object {
const val BASE_URL = "https://api.ssllabs.com/api/v3/"
}
}

View File

@@ -0,0 +1,37 @@
package okhttp3.survey.ssllabs
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class UserAgentCapabilities(
val abortsOnUnrecognizedName: Boolean,
val alpnProtocols: List<String>,
val ellipticCurves: List<Int>,
val handshakeFormat: String,
val hexHandshakeBytes: String,
val highestProtocol: Int,
val id: Int,
val isGrade0: Boolean,
val lowestProtocol: Int,
val maxDhBits: Int,
val maxRsaBits: Int,
val minDhBits: Int,
val minEcdsaBits: Int,
val minRsaBits: Int,
val name: String,
val npnProtocols: List<String>,
val platform: String?,
val requiresSha2: Boolean,
val signatureAlgorithms: List<Int>,
val suiteIds: List<Int>,
val suiteNames: List<String>,
val supportsCompression: Boolean,
val supportsNpn: Boolean,
val supportsRi: Boolean,
val supportsSni: Boolean,
val supportsStapling: Boolean,
val supportsTickets: Boolean,
val userAgent: String?,
val version: String
)

View File

@@ -0,0 +1,27 @@
/*
* Copyright (C) 2016 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 okhttp3.survey.types
data class Client(
val userAgent: String,
val version: String,
val platform: String?,
val enabled: List<SuiteId> = listOf(),
val supported: List<SuiteId> = listOf()
) {
val nameAndVersion: String
get() = "$userAgent/$version"
}

View File

@@ -0,0 +1,19 @@
/*
* Copyright (C) 2016 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 okhttp3.survey.types
data class Record(val java: String, val android: String)

View File

@@ -0,0 +1,24 @@
/*
* Copyright (C) 2016 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 okhttp3.survey.types
import okio.ByteString
data class SuiteId(val id: ByteString?, val name: String) {
fun matches(suiteId: SuiteId): Boolean {
return id == suiteId.id || name.substring(4) == suiteId.name.substring(4)
}
}

View File

@@ -0,0 +1,13 @@
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256
TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA
TLS_RSA_WITH_AES_128_GCM_SHA256
TLS_RSA_WITH_AES_256_GCM_SHA384
TLS_RSA_WITH_AES_128_CBC_SHA
TLS_RSA_WITH_AES_256_CBC_SHA
TLS_RSA_WITH_3DES_EDE_CBC_SHA

View File

@@ -0,0 +1,18 @@
TLS_AES_128_GCM_SHA256
TLS_AES_256_GCM_SHA384
TLS_CHACHA20_POLY1305_SHA256
TLS_AES_128_CCM_SHA256
TLS_AES_256_CCM_8_SHA256
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256
TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA
TLS_RSA_WITH_AES_128_GCM_SHA256
TLS_RSA_WITH_AES_256_GCM_SHA384
TLS_RSA_WITH_AES_128_CBC_SHA
TLS_RSA_WITH_AES_256_CBC_SHA
TLS_RSA_WITH_3DES_EDE_CBC_SHA

View File

@@ -0,0 +1,16 @@
TLS_AES_128_GCM_SHA256
TLS_AES_256_GCM_SHA384
TLS_CHACHA20_POLY1305_SHA256
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256
TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA
TLS_RSA_WITH_AES_128_GCM_SHA256
TLS_RSA_WITH_AES_256_GCM_SHA384
TLS_RSA_WITH_AES_128_CBC_SHA
TLS_RSA_WITH_AES_256_CBC_SHA
TLS_RSA_WITH_3DES_EDE_CBC_SHA

View File

@@ -0,0 +1,15 @@
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256
TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA
TLS_RSA_WITH_AES_128_GCM_SHA256
TLS_RSA_WITH_AES_256_GCM_SHA384
TLS_RSA_WITH_AES_128_CBC_SHA
TLS_RSA_WITH_AES_256_CBC_SHA
TLS_RSA_WITH_3DES_EDE_CBC_SHA

View File

@@ -0,0 +1,16 @@
TLS_AES_128_GCM_SHA256
TLS_AES_256_GCM_SHA384
TLS_CHACHA20_POLY1305_SHA256
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256
TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA
TLS_RSA_WITH_AES_128_GCM_SHA256
TLS_RSA_WITH_AES_256_GCM_SHA384
TLS_RSA_WITH_AES_128_CBC_SHA
TLS_RSA_WITH_AES_256_CBC_SHA
TLS_RSA_WITH_3DES_EDE_CBC_SHA

View File

@@ -41,6 +41,7 @@ include(":samples:guide")
include(":samples:simple-client")
include(":samples:slack")
include(":samples:static-server")
include(":samples:tlssurvey")
include(":samples:unixdomainsockets")
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")