OkHttp 支持接受自签名的 SSL 证书吗?

我为一个客户工作,他的服务器有自签名的 SSL 证书。

我使用的是卢克菲特 + 自定义客户端,使用的是包装好的 OkHttp 客户端:

RestAdapter restAdapter = new RestAdapter.Builder().setEndpoint(Config.BASE_URL + Config.API_VERSION)
.setClient(new CustomClient(new OkClient(), context))
.build();

默认情况下 OkHttp 支持调用自签名 SSL 证书服务器吗?

顺便说一句。默认情况下哪个客户端正在使用 Revifit?我以为它是 OkHttp,但当我研究了一点,我意识到我需要导入 OkHttp 依赖关系

104432 次浏览

Yes, It does.

Retrofit allows you to set your custom HTTP client, that is configured to your needs.

As for self-signed SSL certs there is a discussion here. The link contains code samples to add self-signed SSL to Android's DefaultHttpClient and to load this client to Retrofit.

If you need OkHttpClient to accept self signed SSL, you need to pass it custom javax.net.ssl.SSLSocketFactory instance via setSslSocketFactory(SSLSocketFactory sslSocketFactory) method.

The easiest method to get a socket factory is to get one from javax.net.ssl.SSLContext as discussed here.

Here is a sample for configuring OkHttpClient:

OkHttpClient client = new OkHttpClient();
KeyStore keyStore = readKeyStore(); //your method to obtain KeyStore
SSLContext sslContext = SSLContext.getInstance("SSL");
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, "keystore_pass".toCharArray());
sslContext.init(keyManagerFactory.getKeyManagers(),trustManagerFactory.getTrustManagers(), new SecureRandom());
client.setSslSocketFactory(sslContext.getSocketFactory());

Updated code for okhttp3 (using builder):

    OkHttpClient client = new OkHttpClient.Builder()
.sslSocketFactory(sslContext.getSocketFactory())
.build();

the client here is now configured to use certificates from your KeyStore. However it will only trust the certificates in your KeyStore and will not trust anything else, even if your system trust them by default. (If you have only self signed certs in your KeyStore and try to connect to Google main page via HTTPS you will get SSLHandshakeException).

You can obtain KeyStore instance from file as seen in docs:

KeyStore readKeyStore() {
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());


// get user password and file input stream
char[] password = getPassword();


java.io.FileInputStream fis = null;
try {
fis = new java.io.FileInputStream("keyStoreName");
ks.load(fis, password);
} finally {
if (fis != null) {
fis.close();
}
}
return ks;
}

If you are on android you can put it in res/raw folder and get it from a Context instance using

fis = context.getResources().openRawResource(R.raw.your_keystore_filename);

There are several discussions on how to create your keystore. For example here

The following piece of code allows you to create an OkHttp client that can be used with Retrofit. Mailmustdie's answer is "better" in the sense that it is more secure, but the code snippet below is faster to implement

import com.squareup.okhttp.Headers;
import com.squareup.okhttp.MediaType;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.ResponseBody;


import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;


import okio.BufferedSink;
import retrofit.client.Header;
import retrofit.client.OkClient;
import retrofit.client.Request;
import retrofit.client.Response;
import retrofit.mime.TypedInput;
import retrofit.mime.TypedOutput;


import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;


import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;


public class TrustingOkClient extends OkClient {


static final int CONNECT_TIMEOUT_MILLIS = 15 * 1000; // 15s
static final int READ_TIMEOUT_MILLIS = 20 * 1000; // 20s


private static OkHttpClient generateDefaultOkHttp() {
OkHttpClient client = new OkHttpClient();
client.setConnectTimeout(CONNECT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
client.setReadTimeout(READ_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);






final TrustManager[] certs = new TrustManager[]{new X509TrustManager() {


@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}


@Override
public void checkServerTrusted(final X509Certificate[] chain,
final String authType) throws CertificateException {
}


@Override
public void checkClientTrusted(final X509Certificate[] chain,
final String authType) throws CertificateException {
}
}};


SSLContext ctx = null;
try {
ctx = SSLContext.getInstance("TLS");
ctx.init(null, certs, new SecureRandom());
} catch (final java.security.GeneralSecurityException ex) {
}


try {
final HostnameVerifier hostnameVerifier = new HostnameVerifier() {
@Override
public boolean verify(final String hostname,
final SSLSession session) {
return true;
}
};
client.setHostnameVerifier(hostnameVerifier);
client.setSslSocketFactory(ctx.getSocketFactory());
} catch (final Exception e) {
}
return client;
}


private final OkHttpClient client;


public TrustingOkClient() {
this(generateDefaultOkHttp());
}


public TrustingOkClient(OkHttpClient client) {
if (client == null) throw new NullPointerException("client == null");
this.client = client;
}


@Override public Response execute(Request request) throws IOException {
return parseResponse(client.newCall(createRequest(request)).execute());
}


static com.squareup.okhttp.Request createRequest(Request request) {
com.squareup.okhttp.Request.Builder builder = new com.squareup.okhttp.Request.Builder()
.url(request.getUrl())
.method(request.getMethod(), createRequestBody(request.getBody()));


List<Header> headers = request.getHeaders();
for (int i = 0, size = headers.size(); i < size; i++) {
Header header = headers.get(i);
String value = header.getValue();
if (value == null) value = "";
builder.addHeader(header.getName(), value);
}


return builder.build();
}


static Response parseResponse(com.squareup.okhttp.Response response) {
return new Response(response.request().urlString(), response.code(), response.message(),
createHeaders(response.headers()), createResponseBody(response.body()));
}


private static RequestBody createRequestBody(final TypedOutput body) {
if (body == null) {
return null;
}
final MediaType mediaType = MediaType.parse(body.mimeType());
return new RequestBody() {
@Override public MediaType contentType() {
return mediaType;
}


@Override public void writeTo(BufferedSink sink) throws IOException {
body.writeTo(sink.outputStream());
}


@Override public long contentLength() {
return body.length();
}
};
}


private static TypedInput createResponseBody(final ResponseBody body) {
try {
if (body.contentLength() == 0) {
return null;
}
return new TypedInput() {
@Override public String mimeType() {
MediaType mediaType = body.contentType();
return mediaType == null ? null : mediaType.toString();
}


@Override public long length() {
try {
return body.contentLength();
} catch (Exception exception) {
System.out.println(exception.toString());
}
throw new Error("createResponseBody has invalid length for its response");
}


@Override public InputStream in() throws IOException {
return body.byteStream();
}
};
} catch (Exception exception) {
System.out.println(exception.toString());
}
throw new Error("createResponseBody has invalid content length for its response");
}


private static List<Header> createHeaders(Headers headers) {
int size = headers.size();
List<Header> headerList = new ArrayList<Header>(size);
for (int i = 0; i < size; i++) {
headerList.add(new Header(headers.name(i), headers.value(i)));
}
return headerList;
}


}

For okhttp3.OkHttpClient Version com.squareup.okhttp3:okhttp:3.2.0 you have to use the code below :

import okhttp3.Call;
import okhttp3.Cookie;
import okhttp3.CookieJar;
import okhttp3.Headers;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;


......


OkHttpClient.Builder clientBuilder = client.newBuilder().readTimeout(LOGIN_TIMEOUT_SEC, TimeUnit.SECONDS);


boolean allowUntrusted = true;


if (  allowUntrusted) {
Log.w(TAG,"**** Allow untrusted SSL connection ****");
final TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() {
@Override
public X509Certificate[] getAcceptedIssuers() {
X509Certificate[] cArrr = new X509Certificate[0];
return cArrr;
}


@Override
public void checkServerTrusted(final X509Certificate[] chain,
final String authType) throws CertificateException {
}


@Override
public void checkClientTrusted(final X509Certificate[] chain,
final String authType) throws CertificateException {
}
}};


SSLContext sslContext = SSLContext.getInstance("SSL");


sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
clientBuilder.sslSocketFactory(sslContext.getSocketFactory());


HostnameVerifier hostnameVerifier = new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
Log.d(TAG, "Trust Host :" + hostname);
return true;
}
};
clientBuilder.hostnameVerifier( hostnameVerifier);
}


final Call call = clientBuilder.build().newCall(request);

Two methods from our app to get OkHttpClient 3.0 instance that recognizes your self-signed certificates from your keystore (uses prepared pkcs12 certificate file in your Android project "raw" resources folder):

private static OkHttpClient getSSLClient(Context context) throws
NoSuchAlgorithmException,
KeyStoreException,
KeyManagementException,
CertificateException,
IOException {


OkHttpClient client;
SSLContext sslContext;
SSLSocketFactory sslSocketFactory;
TrustManager[] trustManagers;
TrustManagerFactory trustManagerFactory;
X509TrustManager trustManager;


trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(readKeyStore(context));
trustManagers = trustManagerFactory.getTrustManagers();


if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
throw new IllegalStateException("Unexpected default trust managers:" + Arrays.toString(trustManagers));
}


trustManager = (X509TrustManager) trustManagers[0];


sslContext = SSLContext.getInstance("TLS");


sslContext.init(null, new TrustManager[]{trustManager}, null);


sslSocketFactory = sslContext.getSocketFactory();


client = new OkHttpClient.Builder()
.sslSocketFactory(sslSocketFactory, trustManager)
.build();
return client;
}


/**
* Get keys store. Key file should be encrypted with pkcs12 standard. It    can be done with standalone encrypting java applications like "keytool". File password is also required.
*
* @param context Activity or some other context.
* @return Keys store.
* @throws KeyStoreException
* @throws CertificateException
* @throws NoSuchAlgorithmException
* @throws IOException
*/
private static KeyStore readKeyStore(Context context) throws
KeyStoreException,
CertificateException,
NoSuchAlgorithmException,
IOException {
KeyStore keyStore;
char[] PASSWORD = "12345678".toCharArray();
ArrayList<InputStream> certificates;
int certificateIndex;
InputStream certificate;


certificates = new ArrayList<>();
certificates.add(context.getResources().openRawResource(R.raw.ssl_pkcs12));


keyStore = KeyStore.getInstance("pkcs12");


for (Certificate certificate : certificates) {
try {
keyStore.load(certificate, PASSWORD);
} finally {
if (certificate != null) {
certificate.close();
}
}
}
return keyStore;
}

Against Retrofit 1.9 I was able to accept any certificate with the following strategy: use at your own risk! Accepting any certificate is dangerous and you should understand the consequences. Some relevant parts come from org.apache.http.ssl, so you may require some imports here.

// ...


Client httpClient = getHttpClient();


RestAdapter adapter = new RestAdapter.Builder()
.setClient(httpClient)
// ... the rest of your builder setup
.build();


// ...


private Client getHttpClient() {
try {
// Allow self-signed (and actually any) SSL certificate to be trusted in this context
TrustStrategy acceptingTrustStrategy = (X509Certificate[] chain, String authType) -> true;


SSLContext sslContext = org.apache.http.ssl.SSLContexts.custom()
.loadTrustMaterial(null, acceptingTrustStrategy)
.build();


sslContext.getSocketFactory();


SSLSocketFactory sf = sslContext.getSocketFactory();


OkHttpClient client = new OkHttpClient();
client.setSslSocketFactory(sf);


return new OkClient(client);
} catch (Exception e) {
throw new RuntimeException("Failed to create new HTTP client", e);
}
}

I know that this post is quite old, bui i want to share the solution that worked for me with the latest update of OkHttp, the 3.12.1 version in the time i'm writing.

First of all you need to obtain the KeyStore object that will be then added to the TrustManager:

/**
*  @param context The Android context to be used for retrieving the keystore from raw resource
* @return the KeyStore read or null on error
*/
private static KeyStore readKeyStore(Context context) {


char[] password = "keystore_password".toCharArray();


// for non-android usage:
// try(FileInputStream is = new FileInputStream(keystoreName)) {


try(InputStream is = context.getResources().openRawResource(R.raw.keystore)) {
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(is, password);
return ks;
} catch (CertificateException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (KeyStoreException e) {
e.printStackTrace();
}


return null;
}

Now you can get the builded OkHttpClient with the self-signed certificate in your keystore:

/**
* @param context The Android context used to obtain the KeyStore
* @return the builded OkHttpClient or null on error
*/
public static OkHttpClient getOkHttpClient(Context context) {


try {
TrustManagerFactory trustManagerFactory = TrustManagerFactory
.getInstance(TrustManagerFactory.getDefaultAlgorithm());


trustManagerFactory.init(readKeyStore(context));


X509TrustManager trustManager = (X509TrustManager) trustManagerFactory.getTrustManagers()[0];
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[]{trustManager}, null);


return new OkHttpClient.Builder()
.hostnameVerifier((hostname, session) -> {
HostnameVerifier hv = HttpsURLConnection.getDefaultHostnameVerifier();
/* Never return true without verifying the hostname, otherwise you will be vulnerable
to man in the middle attacks. */
return  hv.verify("your_hostname_here", session);
})
.sslSocketFactory(sslContext.getSocketFactory(), trustManager)
.build();


} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (KeyStoreException e) {
e.printStackTrace();
} catch (KeyManagementException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}


return null;
}

Remember that it is highly discouraged to return always true in the hostnameVerifier to avoid risk of man in the middle attacks.

I had the same problem and I fixed it with the okhttp client as follow:

1.) Add the certificate file to src/main/res/raw/, which includes this content:

-----BEGIN CERTIFICATE-----
...=
-----END CERTIFICATE-----

2.) Instanciate the okHttpClient:

OkHttpClient client = new OkHttpClient.Builder()
.sslSocketFactory(getSslContext(context).getSocketFactory())
.build();

3.) Here is the used getSslContext(Context context) method:

SSLContext getSslContext(Context context) throws Exception {
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); // "BKS"
ks.load(null, null);


InputStream is = context.getResources().openRawResource(R.raw.certificate);
String certificate = Converter.convertStreamToString(is);


// generate input stream for certificate factory
InputStream stream = IOUtils.toInputStream(certificate);


// CertificateFactory
CertificateFactory cf = CertificateFactory.getInstance("X.509");
// certificate
Certificate ca;
try {
ca = cf.generateCertificate(stream);
} finally {
is.close();
}


ks.setCertificateEntry("my-ca", ca);


// TrustManagerFactory
String algorithm = TrustManagerFactory.getDefaultAlgorithm();
TrustManagerFactory tmf = TrustManagerFactory.getInstance(algorithm);
// Create a TrustManager that trusts the CAs in our KeyStore
tmf.init(ks);


// Create a SSLContext with the certificate that uses tmf (TrustManager)
sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, tmf.getTrustManagers(), new SecureRandom());


return sslContext;
}

If there is the need to add multiple certificates to the SslContext, here is the solution.

Another thing to note, if you pre-install the CA on the device, you can make regular https calls with OKHttp, and no special ssl hoops. The key is to add the network security configs to your manifest.

The key for me to know to do this was that I was getting the following exception.

"Trust anchor for certification path not found."

Here is a good article from Google about how to configure it. https://developer.android.com/training/articles/security-config

Here is an example of my network_security_config.xml

<?xml version="1.0" encoding="UTF-8" ?>
<network-security-config>
<base-config cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="user"/>
<certificates src="system"/>
</trust-anchors>
</base-config>
</network-security-config>

I find answer from :

https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/CustomTrust.java

It uses HandshakeCertificates to add certificates.

 HandshakeCertificates certificates = new HandshakeCertificates.Builder()
.addTrustedCertificate(letsEncryptCertificateAuthority)
.addTrustedCertificate(entrustRootCertificateAuthority)
.addTrustedCertificate(comodoRsaCertificationAuthority)
// Uncomment if standard certificates are also required.
//.addPlatformTrustedCertificates()
.build();


client = new OkHttpClient.Builder()
.sslSocketFactory(certificates.sslSocketFactory(), certificates.trustManager())
.build();

If you have trust certificates in store, you can use it as below:

.......
List<X509Certificate> certificates = getCertificatesFromTrustStore();
        

Builder certificateBuilder =  new HandshakeCertificates.Builder();
for (X509Certificate x509Certificate : certificates) {
certificateBuilder.addTrustedCertificate(x509Certificate);
}
HandshakeCertificates handshakeCertificates =  certificateBuilder.build();
.......
//To get certificates from a keystore
private List<X509Certificate> getCertificatesFromTrustStore() throws Exception {
KeyStore truststore = KeyStore.getInstance("JKS");
truststore.load(new FileInputStream("d:\certs.jsk"), "mypassword".toCharArray());
        

PKIXParameters params = new PKIXParameters(truststore);
Set<TrustAnchor> trustAnchors = params.getTrustAnchors();
LOG.debug("{} certificates found in {} which will be used", trustAnchors.size(), trustStorePath);
        

List<X509Certificate> certificates = trustAnchors.stream()
.map(TrustAnchor::getTrustedCert)
.collect(Collectors.toList());
return certificates;
}

If you need to provide your own certificate, you can pass it like this:

Manifest:

<application android:networkSecurityConfig="@xml/network_security_config"
... >

res/xml/network_security_config.xml:

<?xml version="1.0" encoding="UTF-8" ?>
<network-security-config>
<base-config cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="@raw/your_PEM_formatted_cert" />
<certificates src="user" />
<certificates src="system" />
</trust-anchors>
</base-config>