2

I am planning to use Jamendo API to download music but upon connection to the API the following error was thrown

javax.net.ssl.SSLHandshakeException: Handshake failed
        at com.android.org.conscrypt.ConscryptFileDescriptorSocket.startHandshake(ConscryptFileDescriptorSocket.java:286)
        at okhttp3.internal.connection.RealConnection.connectTls(RealConnection.kt:351)
        at okhttp3.internal.connection.RealConnection.establishProtocol(RealConnection.kt:310)
        at okhttp3.internal.connection.RealConnection.connect(RealConnection.kt:178)
        at okhttp3.internal.connection.ExchangeFinder.findConnection(ExchangeFinder.kt:236)
        at okhttp3.internal.connection.ExchangeFinder.findHealthyConnection(ExchangeFinder.kt:109)
        at okhttp3.internal.connection.ExchangeFinder.find(ExchangeFinder.kt:77)
        at okhttp3.internal.connection.Transmitter.newExchange$okhttp(Transmitter.kt:162)
        at okhttp3.internal.connection.ConnectInterceptor.intercept(ConnectInterceptor.kt:35)
        at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:112)
        at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:87)
        at okhttp3.internal.cache.CacheInterceptor.intercept(CacheInterceptor.kt:82)
        at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:112)
        at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:87)
        at okhttp3.internal.http.BridgeInterceptor.intercept(BridgeInterceptor.kt:84)
        at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:112)
        at okhttp3.internal.http.RetryAndFollowUpInterceptor.intercept(RetryAndFollowUpInterceptor.kt:71)
        at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:112)
        at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:87)
        at okhttp3.RealCall.getResponseWithInterceptorChain(RealCall.kt:184)
        at okhttp3.RealCall.execute(RealCall.kt:66)
        at com.example.musicplayer.utils.CertificatePinningKt.certificatePinning(CertificatePinning.kt:26)
        at com.example.musicplayer.fragments.HomeFragment$onActivityCreated$1$1.invokeSuspend(HomeFragment.kt:42)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:241)
        at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:594)
        at kotlinx.coroutines.scheduling.CoroutineScheduler.access$runSafely(CoroutineScheduler.kt:60)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:740)
     Caused by: javax.net.ssl.SSLProtocolException: SSL handshake aborted: ssl=0xedb6ee48: Failure in SSL library, usually a protocol error
    error:100000f0:SSL routines:OPENSSL_internal:UNSUPPORTED_PROTOCOL (external/boringssl/src/ssl/handshake_client.cc:576 0xe5faba43:0x00000000)
        at com.android.org.conscrypt.NativeCrypto.SSL_do_handshake(Native Method)
        at com.android.org.conscrypt.NativeSsl.doHandshake(NativeSsl.java:375)
        at com.android.org.conscrypt.ConscryptFileDescriptorSocket.startHandshake(ConscryptFileDescriptorSocket.java:224)
            ... 27 more

Then I came to know that certain CA certificates are not known to Android and the way out is either enable TrustManager to not verify any certificate on an https request like the accepted answer of this SO post or to add a custom TrustManager to accept the CA certificate of the server to which I need to communicate according to this google doc , and I took the latter approach.

!) First, I checked the Jamendo's server info using the following command

$ openssl s_client -connect jamendo.com:443 | openssl x509 -noout -subject -issuer

which results in

depth=3 C = US, O = "The Go Daddy Group, Inc.", OU = Go Daddy Class 2 Certification Authority verify return:1
depth=2 C = US, ST = Arizona, L = Scottsdale, O = "GoDaddy.com, Inc.", CN = Go Daddy Root Certificate Authority - G2 verify return:1
depth=1 C = US, ST = Arizona, L = Scottsdale, O = "GoDaddy.com, Inc.", OU = http://certs.godaddy.com/repository/, CN = Go Daddy Secure Certificate Authority - G2 verify return:1
depth=0 C = LU, L = Luxembourg, O = Jamendo SA, CN = .jamendo.com verify return:1
subject= /C=LU/L=Luxembourg/O=Jamendo SA/CN=
.jamendo.com issuer= /C=US/ST=Arizona/L=Scottsdale/O=GoDaddy.com, Inc./OU=http://certs.godaddy.com/repository//CN=Go Daddy Secure Certificate Authority - G2

It looks like that Jamendo is using GoDaddy's hosting service so I went to their certification page and as in the above output, the certificates used are from G2 group so I downloaded root and intermediate certificates(first two) as they are mentioned in the output.

2) And then I tried to create custom SSLSocketFactory and TrustManager which results into the same error. The approach is shown in the following code

fun getCACertificateAndroid(resources: Resources): Pair<SSLSocketFactory, X509TrustManager> {
    val cf: CertificateFactory = CertificateFactory.getInstance("X.509")

    //  certificate 1
    var caInput: InputStream = resources.openRawResource(R.raw.gdig2)
    val ca: X509Certificate = caInput.use {
        cf.generateCertificate(it) as X509Certificate
    }
    Log.d("GetCACertificateAndroid", "getCACertificateAndroid: ca= + ${ca.subjectDN}")

    //  certificate 2
    caInput = resources.openRawResource(R.raw.gdroot_g2)
    val ca1: X509Certificate = caInput.use {
        cf.generateCertificate(it) as X509Certificate
    }
    Log.d("GetCACertificateAndroid", "getCACertificateAndroid: ca1= + ${ca1.subjectDN}")

    // Create a KeyStore containing our trusted CAs
    val keyStoreType = KeyStore.getDefaultType()
    val keyStore = KeyStore.getInstance(keyStoreType).apply {
        load(null, null)
        setCertificateEntry("ca", ca)
        setCertificateEntry("ca1", ca1)
    }

    // Create a TrustManager that trusts the CAs inputStream our KeyStore
    val tmfAlgorithm: String = TrustManagerFactory.getDefaultAlgorithm()
    val tmf: TrustManagerFactory = TrustManagerFactory.getInstance(tmfAlgorithm).apply {
        init(keyStore)
    }

    // Create an SSLContext that uses our TrustManager
    val context: SSLContext = SSLContext.getInstance("TLS").apply {
        init(null, tmf.trustManagers, null)
    }


    return Pair(context.socketFactory, tmf.trustManagers[0] as X509TrustManager)
}

Some other file

...

val (_sslSocketFactory, x509tTrustManager) = getCACertificateAndroid(resources)

    val client = OkHttpClient.Builder()
        .sslSocketFactory(_sslSocketFactory, x509tTrustManager)
        .build()

    val gson = GsonBuilder()
        .setLenient()
        .create()

    val retrofit = Retrofit.Builder()
        .baseUrl("https://api.jamendo.com/v3.0/playlists/?client_id=c7668145&format=jsonpretty&namesearch=cool") //   "https://storage.googleapis.com/"
        .addConverterFactory(GsonConverterFactory.create(gson))
        .client(client)
        .build()

...

3) Then I came to know about Certificate pinning in OKHttp and thought to give it a try by getting the base64 encoding of the downloaded certificates by running the following command

openssl x509 -in cert.crt -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64

based on the accepted answer of this post and then tried to run the following code but still, the error persists.

fun certificatePinning() {
    val hostname = "api.jamendo.com/v3.0/playlists/?client_id=c7668145&format=jsonpretty&namesearch=cool"
    val sha256base64_hash1 = "sha256/Ko8tivDrEjiY90yGasP6ZpBU4jwXvHqVvQI0GS3GNdA="
    val sha256base64_hash2 = "sha256/8Rw90Ej3Ttt8RRkrg+WYDS9n7IS03bk5bjP/UXPtaY8="


    val certificatePinner = CertificatePinner.Builder()
        .add(hostname, sha256base64_hash1)
        .add(hostname, sha256base64_hash2)
        .build()
    val client = OkHttpClient.Builder()
        .certificatePinner(certificatePinner)
        .build()

    val request = Request.Builder()
        .url("https://$hostname")
        .build()
    client.newCall(request).execute()
}

If someone could point me in the right direction then it would be a great help.

Neeraj Sewani
  • 3,952
  • 6
  • 38
  • 55

2 Answers2

2

The handshake issue is due to Jamendo API using an old deprecated TLS protocol version (1.0) and not support newer protocol versions:
* https://github.com/square/okhttp/issues/4670 * https://medium.com/square-corner-blog/okhttp-3-13-requires-android-5-818bb78d07ce

Side-notes: I would definitely opt against a custom TrustManager implementation, this would only make sense e.g. if your endpoint is using a self-signed certificate. As a basic check i would verify that your Android System TrustStore is working by trying to open the Jamendo URL directly on the phone/emulator browser to see if you get any issues? Pinning provides additional protection but does not resolve basic handshake issue you are seeing.

nysos
  • 36
  • 5
  • I am getting a JSON response back upon accessing api.jamendo.com/v3.0/playlists/?client_id=c7668145&format=jsonpretty&namesearch=cool on default phone browser as well as on chrome – Neeraj Sewani Sep 11 '19 at 06:37
  • Ok thats good, got somewhat sidetracked by the pinning discussion. So based on your first stacktrace - the handshake fails which is neither a pinning nor a trust issue. Jamendo does not seem to support up to date TLS protocols/ciphers: [api.jamendo.com at ssllabs](https://www.ssllabs.com/ssltest/analyze.html?d=api.jamendo.com). – nysos Sep 11 '19 at 06:54
  • Thanks for providing the link to sslabs. And you are right, they only support TLS v1.0 which will be deprecated by all the major internet companies as written here https://en.wikipedia.org/wiki/Transport_Layer_Security. Moreover, Retrofit has already stopped the support for TLS v1.0 as I found out in their Medium article here https://medium.com/square-corner-blog/okhttp-3-13-requires-android-5-818bb78d07ce. And thanks for pointing out that "Protocol error" in the stacktrace. – Neeraj Sewani Sep 12 '19 at 05:44
  • If you could modify your answer by including these links https://github.com/square/okhttp/issues/4670 and https://medium.com/square-corner-blog/okhttp-3-13-requires-android-5-818bb78d07ce plus your comment then that would be the right answer and would help people to know more about this problem as no such question exists on SO. – Neeraj Sewani Sep 12 '19 at 05:48
1

Check your 'TLS' support. I've faced with this problem. In my app I use retrofit lib. So try add 'COMPATIBLE_TLS' config to your OkHttpClient like:

OkHttpClient client = new OkHttpClient();
List<ConnectionSpec> connectionSpecs = new ArrayList<>();
connectionSpecs.add(ConnectionSpec.COMPATIBLE_TLS);
client.setConnectionSpecs(connectionSpecs);
...

And update your library in gradle at least to v2.7.5 like:

implementation 'com.squareup.okhttp:okhttp:2.7.5'
Djek-Grif
  • 1,391
  • 18
  • 18