refactor cert handling for Android (preparing for v2 changes)

This commit is contained in:
Sefa Ilkimen
2018-04-11 03:24:27 +02:00
parent 32fdf49d31
commit 2ae4c7cf39
4 changed files with 150 additions and 168 deletions

View File

@@ -263,6 +263,12 @@ public class HttpRequest {
*/
public static final String PARAM_CHARSET = "charset";
public static final String CERT_MODE_DEFAULT = "default";
public static final String CERT_MODE_PINNED = "pinned";
public static final String CERT_MODE_TRUSTALL = "trustall";
private static final String BOUNDARY = "00content0boundary00";
private static final String CONTENT_TYPE_MULTIPART = "multipart/form-data; boundary="
@@ -272,13 +278,13 @@ public class HttpRequest {
private static final String[] EMPTY_STRINGS = new String[0];
private static SSLSocketFactory PINNED_FACTORY;
private static SSLSocketFactory SOCKET_FACTORY;
private static SSLSocketFactory TRUSTED_FACTORY;
private static String CURRENT_CERT_MODE = CERT_MODE_DEFAULT;
private static ArrayList<Certificate> PINNED_CERTS;
private static HostnameVerifier TRUSTED_VERIFIER;
private static HostnameVerifier HOSTNAME_VERIFIER;
private static String getValidCharset(final String charset) {
if (charset != null && charset.length() > 0)
@@ -287,63 +293,107 @@ public class HttpRequest {
return CHARSET_UTF8;
}
private static SSLSocketFactory getPinnedFactory()
throws HttpRequestException {
if (PINNED_FACTORY != null) {
return PINNED_FACTORY;
} else {
IOException e = new IOException("You must add at least 1 certificate in order to pin to certificates");
throw new HttpRequestException(e);
/**
* Configure SSL cert handling for all future HTTPS connections
*
* @param mode
*/
public static void setSSLCertMode(String mode) {
try {
if (mode == CERT_MODE_TRUSTALL) {
SOCKET_FACTORY = createSocketFactory(getNoopTrustManagers());
} else if (mode == CERT_MODE_PINNED) {
SOCKET_FACTORY = createSocketFactory(getPinnedTrustManagers());
} else {
SOCKET_FACTORY = null;
}
CURRENT_CERT_MODE = mode;
} catch(IOException e) {
throw new HttpRequestException(e);
}
}
private static SSLSocketFactory getTrustedFactory()
throws HttpRequestException {
if (TRUSTED_FACTORY == null) {
final TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {
/**
* Configure host name verification for all future HTTPS connections
*
* @param enabled
*/
public static void setHostnameVerification(boolean enabled) {
if (enabled) {
HOSTNAME_VERIFIER = null;
} else {
HOSTNAME_VERIFIER = getTrustedVerifier();
}
}
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
public void checkClientTrusted(X509Certificate[] chain, String authType) {
// Intentionally left blank
}
public void checkServerTrusted(X509Certificate[] chain, String authType) {
// Intentionally left blank
}
} };
try {
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, trustAllCerts, new SecureRandom());
if (android.os.Build.VERSION.SDK_INT < 20) {
TRUSTED_FACTORY = new TLSSocketFactory(context);
} else {
TRUSTED_FACTORY = context.getSocketFactory();
}
} catch (GeneralSecurityException e) {
IOException ioException = new IOException(
"Security exception configuring SSL context");
ioException.initCause(e);
throw new HttpRequestException(ioException);
}
private static TrustManager[] getPinnedTrustManagers() throws IOException {
if (PINNED_CERTS == null) {
throw new IOException("You must add at least 1 certificate in order to pin to certificates");
}
return TRUSTED_FACTORY;
try {
String keyStoreType = KeyStore.getDefaultType();
KeyStore keyStore = KeyStore.getInstance(keyStoreType);
keyStore.load(null, null);
for (int i = 0; i < PINNED_CERTS.size(); i++) {
keyStore.setCertificateEntry("CA" + i, PINNED_CERTS.get(i));
}
// Create a TrustManager that trusts the CAs in our KeyStore
String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
tmf.init(keyStore);
return tmf.getTrustManagers();
} catch (GeneralSecurityException e) {
IOException ioException = new IOException("Security exception configuring SSL trust managers");
ioException.initCause(e);
throw new HttpRequestException(ioException);
}
}
private static TrustManager[] getNoopTrustManagers() {
return new TrustManager[] { new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
public void checkClientTrusted(X509Certificate[] chain, String authType) {
// Intentionally left blank
}
public void checkServerTrusted(X509Certificate[] chain, String authType) {
// Intentionally left blank
}
}};
}
private static SSLSocketFactory createSocketFactory(TrustManager[] trustManagers)
throws HttpRequestException {
try {
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, trustManagers, new SecureRandom());
if (android.os.Build.VERSION.SDK_INT < 20) {
return new TLSSocketFactory(context);
} else {
return context.getSocketFactory();
}
} catch (GeneralSecurityException e) {
IOException ioException = new IOException("Security exception configuring SSL context");
ioException.initCause(e);
throw new HttpRequestException(ioException);
}
}
private static HostnameVerifier getTrustedVerifier() {
if (TRUSTED_VERIFIER == null)
TRUSTED_VERIFIER = new HostnameVerifier() {
public boolean verify(String hostname, SSLSession session) {
return true;
}
};
return TRUSTED_VERIFIER;
return new HostnameVerifier() {
public boolean verify(String hostname, SSLSession session) {
return true;
}
};
}
private static StringBuilder addPathSeparator(final String baseUrl,
@@ -453,32 +503,15 @@ public class HttpRequest {
* @throws IOException
*/
public static void addCert(Certificate ca) throws GeneralSecurityException, IOException {
if (PINNED_CERTS == null) {
PINNED_CERTS = new ArrayList<Certificate>();
}
PINNED_CERTS.add(ca);
String keyStoreType = KeyStore.getDefaultType();
KeyStore keyStore = KeyStore.getInstance(keyStoreType);
keyStore.load(null, null);
if (PINNED_CERTS == null) {
PINNED_CERTS = new ArrayList<Certificate>();
}
for (int i = 0; i < PINNED_CERTS.size(); i++) {
keyStore.setCertificateEntry("CA" + i, PINNED_CERTS.get(i));
}
PINNED_CERTS.add(ca);
// Create a TrustManager that trusts the CAs in our KeyStore
String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
tmf.init(keyStore);
// Create an SSLContext that uses our TrustManager
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, tmf.getTrustManagers(), null);
if (android.os.Build.VERSION.SDK_INT < 20) {
PINNED_FACTORY = new TLSSocketFactory(sslContext);
} else {
PINNED_FACTORY = sslContext.getSocketFactory();
}
if (CURRENT_CERT_MODE == CERT_MODE_PINNED) {
SOCKET_FACTORY = createSocketFactory(getPinnedTrustManagers());
}
}
/**
@@ -1632,6 +1665,7 @@ public class HttpRequest {
throw new HttpRequestException(e);
}
this.requestMethod = method;
this.setupSecurity();
}
/**
@@ -1645,6 +1679,23 @@ public class HttpRequest {
throws HttpRequestException {
this.url = url;
this.requestMethod = method;
this.setupSecurity();
}
private void setupSecurity() {
final HttpURLConnection connection = getConnection();
if (!(connection instanceof HttpsURLConnection)) {
return;
}
if (SOCKET_FACTORY != null) {
((HttpsURLConnection) connection).setSSLSocketFactory(SOCKET_FACTORY);
}
if (HOSTNAME_VERIFIER != null) {
((HttpsURLConnection) connection).setHostnameVerifier(HOSTNAME_VERIFIER);
}
}
private Proxy createProxy() {
@@ -3351,58 +3402,6 @@ public class HttpRequest {
return this;
}
/**
* Configure HTTPS connection to trust only certain certificates
* <p>
* This method throws an exception if the current request is not a HTTPS request
*
* @return this request
* @throws HttpRequestException
*/
public HttpRequest pinToCerts() throws HttpRequestException {
final HttpURLConnection connection = getConnection();
if (connection instanceof HttpsURLConnection) {
((HttpsURLConnection) connection).setSSLSocketFactory(getPinnedFactory());
} else {
IOException e = new IOException("You must use a https url to use ssl pinning");
throw new HttpRequestException(e);
}
return this;
}
/**
* Configure HTTPS connection to trust all certificates
* <p>
* This method does nothing if the current request is not a HTTPS request
*
* @return this request
* @throws HttpRequestException
*/
public HttpRequest trustAllCerts() throws HttpRequestException {
final HttpURLConnection connection = getConnection();
if (connection instanceof HttpsURLConnection)
((HttpsURLConnection) connection)
.setSSLSocketFactory(getTrustedFactory());
return this;
}
/**
* Configure HTTPS connection to trust all hosts using a custom
* {@link HostnameVerifier} that always returns <code>true</code> for each
* host verified
* <p>
* This method does nothing if the current request is not a HTTPS request
*
* @return this request
*/
public HttpRequest trustAllHosts() {
final HttpURLConnection connection = getConnection();
if (connection instanceof HttpsURLConnection)
((HttpsURLConnection) connection)
.setHostnameVerifier(getTrustedVerifier());
return this;
}
/**
* Get the {@link URL} of this request's connection
*

View File

@@ -38,10 +38,6 @@ import com.github.kevinsawicki.http.HttpRequest.HttpRequestException;
abstract class CordovaHttp {
protected static final String TAG = "CordovaHTTP";
protected static final String[] ACCEPTED_CHARSETS = new String[] { HttpRequest.CHARSET_UTF8, HttpRequest.CHARSET_LATIN1 };
private static AtomicBoolean sslPinning = new AtomicBoolean(false);
private static AtomicBoolean acceptAllCerts = new AtomicBoolean(false);
private static AtomicBoolean validateDomainName = new AtomicBoolean(true);
private static AtomicBoolean disableRedirect = new AtomicBoolean(false);
private String urlString;
@@ -64,24 +60,6 @@ abstract class CordovaHttp {
this.callbackContext = callbackContext;
}
public static void enableSSLPinning(boolean enable) {
sslPinning.set(enable);
if (enable) {
acceptAllCerts.set(false);
}
}
public static void acceptAllCerts(boolean accept) {
acceptAllCerts.set(accept);
if (accept) {
sslPinning.set(false);
}
}
public static void validateDomainName(boolean accept) {
validateDomainName.set(accept);
}
public static void disableRedirect(boolean disable) {
disableRedirect.set(disable);
}
@@ -122,20 +100,6 @@ abstract class CordovaHttp {
return this.callbackContext;
}
protected HttpRequest setupSecurity(HttpRequest request) {
if (acceptAllCerts.get()) {
request.trustAllCerts();
}
if (!validateDomainName.get()) {
request.trustAllHosts();
}
if (sslPinning.get()) {
request.pinToCerts();
}
return request;
}
protected HttpRequest setupRedirect(HttpRequest request) {
if (disableRedirect.get()) {
request.followRedirects(false);
@@ -222,7 +186,6 @@ abstract class CordovaHttp {
protected void prepareRequest(HttpRequest request) throws HttpRequestException, JSONException {
this.setupRedirect(request);
this.setupSecurity(request);
request.readTimeout(this.getRequestTimeout());
request.acceptCharset(ACCEPTED_CHARSETS);

View File

@@ -98,8 +98,14 @@ public class CordovaHttpPlugin extends CordovaPlugin {
} else if (action.equals("acceptAllCerts")) {
boolean accept = args.getBoolean(0);
CordovaHttp.acceptAllCerts(accept);
CordovaHttp.validateDomainName(!accept);
if (accept) {
HttpRequest.setSSLCertMode(HttpRequest.CERT_MODE_TRUSTALL);
HttpRequest.setHostnameVerification(false);
} else {
HttpRequest.setSSLCertMode(HttpRequest.CERT_MODE_DEFAULT);
HttpRequest.setHostnameVerification(true);
}
callbackContext.success();
} else if (action.equals("uploadFile")) {
String urlString = args.getString(0);
@@ -161,9 +167,12 @@ public class CordovaHttpPlugin extends CordovaPlugin {
InputStream caInput = new BufferedInputStream(in);
HttpRequest.addCert(caInput);
}
CordovaHttp.enableSSLPinning(true);
HttpRequest.setSSLCertMode(HttpRequest.CERT_MODE_PINNED);
HttpRequest.setHostnameVerification(true);
} else {
CordovaHttp.enableSSLPinning(false);
HttpRequest.setSSLCertMode(HttpRequest.CERT_MODE_DEFAULT);
HttpRequest.setHostnameVerification(true);
}
}
}

View File

@@ -437,6 +437,17 @@ const tests = [
validationFunc: function(driver, result) {
result.type.should.be.equal('resolved');
}
},{
description: 'should reject when pinned cert does not match received server cert (GET)',
expected: 'rejected: {"status": -1 ...',
before: helpers.enableSSLPinning,
func: function(resolve, reject) {
cordova.plugin.http.get('https://sha512.badssl.com/', {}, {}, resolve, reject);
},
validationFunc: function(driver, result, targetInfo) {
result.type.should.be.equal('rejected');
result.data.should.be.eql({ status: -1, error: targetInfo.isAndroid ? 'SSL handshake failed' : 'cancelled' });
}
},{
description: 'should send deeply structured JSON object correctly (POST) #65',
expected: 'resolved: {"status": 200, "data": "{\\"data\\": \\"{\\\\"outerObj\\\\":{\\\\"innerStr\\\\":\\\\"testString\\\\",\\\\"innerArr\\\\":[1,2,3]}}\\" ...',