OkHttp的SSL握手溯源

最近研究PKI,想实现私钥不出TEE这个需求,需要确认okhttp中SSL认证的实现,结果说看看源码吧,让我好一顿找阿,特此记录一下过程。

Okhttp添加自签名证书方法:

1
2
3
4
5
6
7
8
9
OkHttpClient.Builder builder = new OkHttpClient.Builder();

// 添加自定义的SSLSocketFactory和X509TrustManager
builder.sslSocketFactory(sslInput.mSSLSocketFactory, sslInput.mX509TrustManager);

// 域名验证
builder.hostnameVerifier((hostname, session) ->
        true
);

最主要的其实就是builder.sslSocketFactory函数,由于是建造者模式,那么OkhttpClient.Builder设置了属性,最终在调用builder.build()函数时,一定会把builder的属性赋值给父类OkhttpClient,追踪builder.build()函数,最终调用OkhttpClient的构造函数,其中有一段逻辑:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
if (builder.sslSocketFactory != null || !isTLS) {
    //如果builder设置了sslSocketFactory就用builder的
    this.sslSocketFactory = builder.sslSocketFactory;
    this.certificateChainCleaner = builder.certificateChainCleaner;
} else {
    // 如果没有设置过builder的ssSocketFactory就用系统默认的
    X509TrustManager trustManager = systemDefaultTrustManager();
    this.sslSocketFactory = systemDefaultSslSocketFactory(trustManager);
    this.certificateChainCleaner = CertificateChainCleaner.get(trustManager);
}

我们主要关注的就是系统默认的systemDefaultSslSocketFactory(trustManager)

继续追踪:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
private SSLSocketFactory systemDefaultSslSocketFactory(X509TrustManager trustManager) {
    try {
      // 获取到平台的SSLContext
      SSLContext sslContext = Platform.get().getSSLContext();
      sslContext.init(null, new TrustManager[] { trustManager }, null);
      return sslContext.getSocketFactory();
    } catch (GeneralSecurityException e) {
      throw assertionError("No System TLS", e); // The system has no TLS. Just give up.
    }
  }

这里就看到了Okhttp的跨平台思路,简单看一下Platform.get()的逻辑:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Platform {

  // 静态加载方式
  private static final Platform PLATFORM = findPlatform();

  public static Platform get() {
    return PLATFORM;
  }

  private static Platform findPlatform() {

    // 优先加载安卓平台
    Platform android = AndroidPlatform.buildIfSupported();

    if (android != null) {
      return android;
    }

    // 其他的都是jdk中的安全策略
    ...
  }
}

那么从这里的逻辑可以看到,各个平台的安全策略可能是不一致的。

那么OKHttp是怎么加载安卓平台的呢:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static Platform buildIfSupported() {
    // Attempt to find Android 2.3+ APIs.
    try {
      Class<?> sslParametersClass;
      try {
        sslParametersClass = Class.forName("com.android.org.conscrypt.SSLParametersImpl");
      } catch (ClassNotFoundException e) {
        // Older platform before being unbundled.
        sslParametersClass = Class.forName(
            "org.apache.harmony.xnet.provider.jsse.SSLParametersImpl");
      }

      ...

      return new AndroidPlatform(sslParametersClass, setUseSessionTickets, setHostname,
          getAlpnSelectedProtocol, setAlpnProtocols);
    } catch (ClassNotFoundException ignored) {
      // This isn't an Android runtime.
    }

    return null;
  }

主要是通过hook系统中的com.android.org.conscrypt.SSLParametersImpl。可以先记住这个类,暂时跳过这里。

SSLContext

我们继续来看SSLContext sslContext = Platform.get().getSSLContext(),向里面追踪:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18

import sun.security.jca.GetInstance;

public SSLContext getSSLContext() {
  try {
    return SSLContext.getInstance("TLS");
  } catch (NoSuchAlgorithmException e) {
    throw new IllegalStateException("No TLS provider", e);
  }
}

public static SSLContext getInstance(String protocol)
            throws NoSuchAlgorithmException {
    GetInstance.Instance instance = GetInstance.getInstance
            ("SSLContext", SSLContextSpi.class, protocol);
    return new SSLContext((SSLContextSpi)instance.impl, instance.provider,
            protocol);
}

发现GetInstance.Instance.impl是SSLContextSpi类型。

到这里GetInstance的源码无法查看,是因为我们没有下载sun.security.jca.GetInstance的源码,但是在AOSP中是可以搜索到的,文件在此

核心方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
 * @param type = "SSLContext"
 * @param clazz = SSLContextSpi.class
 * @param algorithm = "TLS"
 **/
public static Instance getInstance(String type, Class<?> clazz,
        String algorithm) throws NoSuchAlgorithmException {

    ProviderList list = Providers.getProviderList();
    Service firstService = list.getService(type, algorithm);
    if (firstService == null) {
        throw new NoSuchAlgorithmException
                (algorithm + " " + type + " not available");
    }
    NoSuchAlgorithmException failure;
    try {
        return getInstance(firstService, clazz);
    } catch (NoSuchAlgorithmException e) {
        failure = e;
    }
    
    for (Service s : list.getServices(type, algorithm)) {
        if (s == firstService) {
            // do not retry initial failed service
            continue;
        }
        try {
            return getInstance(s, clazz);
        } catch (NoSuchAlgorithmException e) {
            failure = e;
        }
    }
    throw failure;
}

这里主要是用参数中的type与algorithm去做个命中匹配,具体是通过ProvidersProviders.getProviderList()方法完成,而这个类同样位于sun.security.jca包中,在AOSP内是可见的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public static ProviderList getProviderList() {
    ProviderList list = getThreadProviderList();
    if (list == null) {
        list = getSystemProviderList();
    }
    return list;
}

private static void setSystemProviderList(ProviderList list) {
    providerList = list;
}

可惜的是,AOSP中并没有找到调用setSystemProviderList()函数的地方,那就有可能是外部hook调用了。

接下来关注Service firstService = list.getService(type, algorithm);

1
2
3
public List<Service> getServices(String type, String algorithm) {
    return new ServiceList(type, algorithm);
}

…..

最终

是在external/conscrypt/common/src/main/java/org/conscrypt/NativeSsl.java中有具体的握手逻辑,最终是调用:

1
NativeCrypto.SSL_do_handshake(ssl, this, fd, handshakeCallbacks, timeoutMillis);

而这个是个Native方法,具体实现是在external/conscrypt/common/src/jni/main/cpp/conscrypt/native_crypto.cc中实现。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
/**
 * Perform SSL handshake
 */
static void NativeCrypto_SSL_do_handshake(JNIEnv* env, jclass, jlong ssl_address, CONSCRYPT_UNUSED jobject ssl_holder, jobject fdObject,
                                          jobject shc, jint timeout_millis) {
    CHECK_ERROR_QUEUE_ON_RETURN;
    SSL* ssl = to_SSL(env, ssl_address, true);
    JNI_TRACE("ssl=%p NativeCrypto_SSL_do_handshake fd=%p shc=%p timeout_millis=%d", ssl, fdObject,
              shc, timeout_millis);
    if (ssl == nullptr) {
        return;
    }
    if (fdObject == nullptr) {
        conscrypt::jniutil::throwNullPointerException(env, "fd == null");
        JNI_TRACE("ssl=%p NativeCrypto_SSL_do_handshake fd == null => exception", ssl);
        return;
    }
    if (shc == nullptr) {
        conscrypt::jniutil::throwNullPointerException(env, "sslHandshakeCallbacks == null");
        JNI_TRACE("ssl=%p NativeCrypto_SSL_do_handshake sslHandshakeCallbacks == null => exception",
                  ssl);
        return;
    }

    NetFd fd(env, fdObject);
    if (fd.isClosed()) {
        // SocketException thrown by NetFd.isClosed
        JNI_TRACE("ssl=%p NativeCrypto_SSL_do_handshake fd.isClosed() => exception", ssl);
        return;
    }

    int ret = SSL_set_fd(ssl, fd.get());
    JNI_TRACE("ssl=%p NativeCrypto_SSL_do_handshake s=%d", ssl, fd.get());

    if (ret != 1) {
        conscrypt::jniutil::throwSSLExceptionWithSslErrors(env, ssl, SSL_ERROR_NONE,
                                                           "Error setting the file descriptor");
        JNI_TRACE("ssl=%p NativeCrypto_SSL_do_handshake SSL_set_fd => exception", ssl);
        return;
    }

    /*
     * Make socket non-blocking, so SSL_connect SSL_read() and SSL_write() don't hang
     * forever and we can use select() to find out if the socket is ready.
     */
    if (!conscrypt::netutil::setBlocking(fd.get(), false)) {
        conscrypt::jniutil::throwSSLExceptionStr(env, "Unable to make socket non blocking");
        JNI_TRACE("ssl=%p NativeCrypto_SSL_do_handshake setBlocking => exception", ssl);
        return;
    }

    AppData* appData = toAppData(ssl);
    if (appData == nullptr) {
        conscrypt::jniutil::throwSSLExceptionStr(env, "Unable to retrieve application data");
        JNI_TRACE("ssl=%p NativeCrypto_SSL_do_handshake appData => exception", ssl);
        return;
    }

    ret = 0;
    SslError sslError;
    while (appData->aliveAndKicking) {
        errno = 0;

        if (!appData->setCallbackState(env, shc, fdObject)) {
            // SocketException thrown by NetFd.isClosed
            JNI_TRACE("ssl=%p NativeCrypto_SSL_do_handshake setCallbackState => exception", ssl);
            return;
        }
        ret = SSL_do_handshake(ssl);
        appData->clearCallbackState();
        // cert_verify_callback threw exception
        if (env->ExceptionCheck()) {
            ERR_clear_error();
            JNI_TRACE("ssl=%p NativeCrypto_SSL_do_handshake exception => exception", ssl);
            return;
        }
        // success case
        if (ret == 1) {
            break;
        }
        // retry case
        if (errno == EINTR) {
            continue;
        }
        // error case
        sslError.reset(ssl, ret);
        JNI_TRACE(
                "ssl=%p NativeCrypto_SSL_do_handshake ret=%d errno=%d sslError=%d "
                "timeout_millis=%d",
                ssl, ret, errno, sslError.get(), timeout_millis);

        /*
         * If SSL_do_handshake doesn't succeed due to the socket being
         * either unreadable or unwritable, we use sslSelect to
         * wait for it to become ready. If that doesn't happen
         * before the specified timeout or an error occurs, we
         * cancel the handshake. Otherwise we try the SSL_connect
         * again.
         */
        if (sslError.get() == SSL_ERROR_WANT_READ || sslError.get() == SSL_ERROR_WANT_WRITE) {
            appData->waitingThreads++;
            int selectResult = sslSelect(env, sslError.get(), fdObject, appData, timeout_millis);

            if (selectResult == THROWN_EXCEPTION) {
                // SocketException thrown by NetFd.isClosed
                JNI_TRACE("ssl=%p NativeCrypto_SSL_do_handshake sslSelect => exception", ssl);
                return;
            }
            if (selectResult == -1) {
                conscrypt::jniutil::throwSSLExceptionWithSslErrors(
                        env, ssl, SSL_ERROR_SYSCALL, "handshake error",
                        conscrypt::jniutil::throwSSLHandshakeExceptionStr);
                JNI_TRACE("ssl=%p NativeCrypto_SSL_do_handshake selectResult == -1 => exception",
                          ssl);
                return;
            }
            if (selectResult == 0) {
                conscrypt::jniutil::throwSocketTimeoutException(env, "SSL handshake timed out");
                ERR_clear_error();
                JNI_TRACE("ssl=%p NativeCrypto_SSL_do_handshake selectResult == 0 => exception",
                          ssl);
                return;
            }
        } else {
            // ALOGE("Unknown error %d during handshake", error);
            break;
        }
    }

    // clean error. See SSL_do_handshake(3SSL) man page.
    if (ret == 0) {
        /*
         * The other side closed the socket before the handshake could be
         * completed, but everything is within the bounds of the TLS protocol.
         * We still might want to find out the real reason of the failure.
         */
        if (sslError.get() == SSL_ERROR_NONE ||
            (sslError.get() == SSL_ERROR_SYSCALL && errno == 0) ||
            (sslError.get() == SSL_ERROR_ZERO_RETURN)) {
            conscrypt::jniutil::throwSSLHandshakeExceptionStr(env, "Connection closed by peer");
        } else {
            conscrypt::jniutil::throwSSLExceptionWithSslErrors(
                    env, ssl, sslError.release(), "SSL handshake terminated",
                    conscrypt::jniutil::throwSSLHandshakeExceptionStr);
        }
        JNI_TRACE("ssl=%p NativeCrypto_SSL_do_handshake clean error => exception", ssl);
        return;
    }

    // unclean error. See SSL_do_handshake(3SSL) man page.
    if (ret < 0) {
        /*
         * Translate the error and throw exception. We are sure it is an error
         * at this point.
         */
        conscrypt::jniutil::throwSSLExceptionWithSslErrors(
                env, ssl, sslError.release(), "SSL handshake aborted",
                conscrypt::jniutil::throwSSLHandshakeExceptionStr);
        JNI_TRACE("ssl=%p NativeCrypto_SSL_do_handshake unclean error => exception", ssl);
        return;
    }
    JNI_TRACE("ssl=%p NativeCrypto_SSL_do_handshake => success", ssl);
}