From 96f45d727445e82a47e7b856931765124e8fbbf6 Mon Sep 17 00:00:00 2001 From: Sefa Ilkimen Date: Sun, 27 May 2018 19:30:42 +0200 Subject: [PATCH] feature #103: implement HTTP SSL cert modes --- CHANGELOG.md | 6 ++ README.md | 38 +++++--- package.json | 2 +- .../cordovahttp/CordovaHttpPlugin.java | 92 +++++++++---------- src/ios/CordovaHttpPlugin.h | 3 +- src/ios/CordovaHttpPlugin.m | 37 ++++---- test/app-test-definitions.js | 25 +++-- www/advanced-http.js | 9 +- www/helpers.js | 6 ++ www/messages.js | 1 + 10 files changed, 116 insertions(+), 103 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e00fb03..cfb8dab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 2.0.0 + +- Feature #103: implement HTTP SSL cert modes + +- :warning: **Breaking Change**: Removed "enableSSLPinning" and "acceptAllCerts", use "setSSLCertMode" instead. + ## 1.11.1 - Fixed #92: headers not deserialized on platform "browser" diff --git a/README.md b/README.md index f5fd2b8..ad8bb7e 100644 --- a/README.md +++ b/README.md @@ -141,31 +141,45 @@ cordova.plugin.http.clearCookies(); ## Asynchronous Functions These functions all take success and error callbacks as their last 2 arguments. -### enableSSLPinning -Enable or disable SSL pinning. This defaults to false. +### setSSLCertMode +Set SSL Cert handling mode, being one of the following values: + +* `default`: default SSL cert handling using system's CA certs +* `nocheck`: disable SSL cert checking, trusting all certs (meant to be used only for testing purposes) +* `pinned`: trust only provided certs To use SSL pinning you must include at least one .cer SSL certificate in your app project. You can pin to your server certificate or to one of the issuing CA certificates. For ios include your certificate in the root level of your bundle (just add the .cer file to your project/target at the root level). For android include your certificate in your project's platforms/android/assets folder. In both cases all .cer files found will be loaded automatically. If you only have a .pem certificate see this [stackoverflow answer](http://stackoverflow.com/a/16583429/3182729). You want to convert it to a DER encoded certificate with a .cer extension. As an alternative, you can store your .cer files in the www/certificates folder. ```js -cordova.plugin.http.enableSSLPinning(true, function() { +// enable SSL pinning +cordova.plugin.http.setSSLCertMode('pinned', function() { + console.log('success!'); +}, function() { + console.log('error :('); +}); + +// use system's default CA certs +cordova.plugin.http.setSSLCertMode('default', function() { + console.log('success!'); +}, function() { + console.log('error :('); +}); + +// disable SSL cert checking, only meant for testing purposes, do NOT use in production! +cordova.plugin.http.setSSLCertMode('nocheck', function() { console.log('success!'); }, function() { console.log('error :('); }); ``` +### enableSSLPinning +This function was removed in 2.0.0. Use ["setSSLCertMode"](#setSSLCertMode) to enable SSL pinning (mode "pinned"). + ### acceptAllCerts -Accept all SSL certificates. Or disable accepting all certificates. This defaults to false. - -```js -cordova.plugin.http.acceptAllCerts(true, function() { - console.log('success!'); -}, function() { - console.log('error :('); -}); -``` +This function was removed in 2.0.0. Use ["setSSLCertMode"](#setSSLCertMode) to disable checking certs (mode "nocheck"). ### disableRedirect If set to `true`, it won't follow redirects automatically. This defaults to false. diff --git a/package.json b/package.json index b1d83d9..4db04ca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cordova-plugin-advanced-http", - "version": "1.11.1", + "version": "2.0.0", "description": "Cordova / Phonegap plugin for communicating with HTTP servers using SSL pinning", "scripts": { "buildbrowser": "./scripts/build-test-app.sh --browser", diff --git a/src/android/com/synconset/cordovahttp/CordovaHttpPlugin.java b/src/android/com/synconset/cordovahttp/CordovaHttpPlugin.java index 9a497f5..ccf218c 100644 --- a/src/android/com/synconset/cordovahttp/CordovaHttpPlugin.java +++ b/src/android/com/synconset/cordovahttp/CordovaHttpPlugin.java @@ -86,25 +86,25 @@ public class CordovaHttpPlugin extends CordovaPlugin { CordovaHttpHead head = new CordovaHttpHead(urlString, params, headers, timeoutInMilliseconds, callbackContext); cordova.getThreadPool().execute(head); - } else if (action.equals("enableSSLPinning")) { - try { - boolean enable = args.getBoolean(0); - this.enableSSLPinning(enable); + } else if (action.equals("setSSLCertMode")) { + String mode = args.getString(0); + + if (mode.equals("default")) { + HttpRequest.setSSLCertMode(HttpRequest.CERT_MODE_DEFAULT); callbackContext.success(); - } catch(Exception e) { - e.printStackTrace(); - callbackContext.error("There was an error setting up ssl pinning"); + } else if (mode.equals("nocheck")) { + HttpRequest.setSSLCertMode(HttpRequest.CERT_MODE_TRUSTALL); + callbackContext.success(); + } else if (mode.equals("pinned")) { + try { + this.loadSSLCerts(); + HttpRequest.setSSLCertMode(HttpRequest.CERT_MODE_PINNED); + callbackContext.success(); + } catch(Exception e) { + e.printStackTrace(); + callbackContext.error("There was an error setting up ssl pinning"); + } } - } else if (action.equals("acceptAllCerts")) { - boolean accept = args.getBoolean(0); - - if (accept) { - HttpRequest.setSSLCertMode(HttpRequest.CERT_MODE_TRUSTALL); - } else { - HttpRequest.setSSLCertMode(HttpRequest.CERT_MODE_DEFAULT); - } - - callbackContext.success(); } else if (action.equals("uploadFile")) { String urlString = args.getString(0); Object params = args.get(1); @@ -125,50 +125,44 @@ public class CordovaHttpPlugin extends CordovaPlugin { cordova.getThreadPool().execute(download); } else if (action.equals("disableRedirect")) { - boolean disable = args.getBoolean(0); - CordovaHttp.disableRedirect(disable); - callbackContext.success(); + boolean disable = args.getBoolean(0); + CordovaHttp.disableRedirect(disable); + callbackContext.success(); } else { return false; } return true; } - private void enableSSLPinning(boolean enable) throws GeneralSecurityException, IOException { - if (enable) { - AssetManager assetManager = cordova.getActivity().getAssets(); - String[] files = assetManager.list(""); - int index; - ArrayList cerFiles = new ArrayList(); - for (int i = 0; i < files.length; i++) { - index = files[i].lastIndexOf('.'); - if (index != -1) { - if (files[i].substring(index).equals(".cer")) { - cerFiles.add(files[i]); - } - } - } - - // scan the www/certificates folder for .cer files as well - files = assetManager.list("www/certificates"); - for (int i = 0; i < files.length; i++) { - index = files[i].lastIndexOf('.'); - if (index != -1) { + private void loadSSLCerts() throws GeneralSecurityException, IOException { + AssetManager assetManager = cordova.getActivity().getAssets(); + String[] files = assetManager.list(""); + int index; + ArrayList cerFiles = new ArrayList(); + for (int i = 0; i < files.length; i++) { + index = files[i].lastIndexOf('.'); + if (index != -1) { if (files[i].substring(index).equals(".cer")) { - cerFiles.add("www/certificates/" + files[i]); + cerFiles.add(files[i]); } - } } + } - for (int i = 0; i < cerFiles.size(); i++) { - InputStream in = cordova.getActivity().getAssets().open(cerFiles.get(i)); - InputStream caInput = new BufferedInputStream(in); - HttpRequest.addCert(caInput); + // scan the www/certificates folder for .cer files as well + files = assetManager.list("www/certificates"); + for (int i = 0; i < files.length; i++) { + index = files[i].lastIndexOf('.'); + if (index != -1) { + if (files[i].substring(index).equals(".cer")) { + cerFiles.add("www/certificates/" + files[i]); } + } + } - HttpRequest.setSSLCertMode(HttpRequest.CERT_MODE_PINNED); - } else { - HttpRequest.setSSLCertMode(HttpRequest.CERT_MODE_DEFAULT); + for (int i = 0; i < cerFiles.size(); i++) { + InputStream in = cordova.getActivity().getAssets().open(cerFiles.get(i)); + InputStream caInput = new BufferedInputStream(in); + HttpRequest.addCert(caInput); } } } diff --git a/src/ios/CordovaHttpPlugin.h b/src/ios/CordovaHttpPlugin.h index 4014626..1a05282 100644 --- a/src/ios/CordovaHttpPlugin.h +++ b/src/ios/CordovaHttpPlugin.h @@ -4,8 +4,7 @@ @interface CordovaHttpPlugin : CDVPlugin -- (void)enableSSLPinning:(CDVInvokedUrlCommand*)command; -- (void)acceptAllCerts:(CDVInvokedUrlCommand*)command; +- (void)setSSLCertMode:(CDVInvokedUrlCommand*)command; - (void)disableRedirect:(CDVInvokedUrlCommand*)command; - (void)post:(CDVInvokedUrlCommand*)command; - (void)get:(CDVInvokedUrlCommand*)command; diff --git a/src/ios/CordovaHttpPlugin.m b/src/ios/CordovaHttpPlugin.m index 8277a27..7694678 100644 --- a/src/ios/CordovaHttpPlugin.m +++ b/src/ios/CordovaHttpPlugin.m @@ -120,23 +120,31 @@ return headerFieldsCopy; } -- (void)setTimeout:(NSTimeInterval)timeout forManager:(AFHTTPSessionManager*)manager { - [manager.requestSerializer setTimeoutInterval:timeout]; -} +- (void)setSSLCertMode:(CDVInvokedUrlCommand*)command { + NSString *certMode = [command.arguments objectAtIndex:0]; -- (void)enableSSLPinning:(CDVInvokedUrlCommand*)command { - bool enable = [[command.arguments objectAtIndex:0] boolValue]; - - if (enable) { - securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate]; - } else { + if ([certMode isEqualToString: @"default"]) { securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeNone]; + securityPolicy.allowInvalidCertificates = NO; + securityPolicy.validatesDomainName = YES; + } else if ([certMode isEqualToString: @"nocheck"]) { + securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeNone]; + securityPolicy.allowInvalidCertificates = YES; + securityPolicy.validatesDomainName = NO; + } else if ([certMode isEqualToString: @"pinned"]) { + securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate]; + securityPolicy.allowInvalidCertificates = NO; + securityPolicy.validatesDomainName = YES; } CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; } +- (void)setTimeout:(NSTimeInterval)timeout forManager:(AFHTTPSessionManager*)manager { + [manager.requestSerializer setTimeoutInterval:timeout]; +} + - (void)disableRedirect:(CDVInvokedUrlCommand*)command { CDVPluginResult* pluginResult = nil; bool disable = [[command.arguments objectAtIndex:0] boolValue]; @@ -147,17 +155,6 @@ [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; } -- (void)acceptAllCerts:(CDVInvokedUrlCommand*)command { - CDVPluginResult* pluginResult = nil; - bool allow = [[command.arguments objectAtIndex:0] boolValue]; - - securityPolicy.allowInvalidCertificates = allow; - securityPolicy.validatesDomainName = !allow; - - pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; - [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; -} - - (void)post:(CDVInvokedUrlCommand*)command { AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; manager.securityPolicy = securityPolicy; diff --git a/test/app-test-definitions.js b/test/app-test-definitions.js index d6493f2..62a2de7 100644 --- a/test/app-test-definitions.js +++ b/test/app-test-definitions.js @@ -1,15 +1,14 @@ const hooks = { onBeforeEachTest: function(done) { cordova.plugin.http.clearCookies(); - cordova.plugin.http.acceptAllCerts(false, function() { - cordova.plugin.http.enableSSLPinning(false, done, done); - }, done); + helpers.setDefaultCertMode(done); } }; const helpers = { - acceptAllCerts: function(done) { cordova.plugin.http.acceptAllCerts(true, done, done); }, - enableSSLPinning: function(done) { cordova.plugin.http.enableSSLPinning(true, done, done); }, + setDefaultCertMode: function(done) { cordova.plugin.http.setSSLCertMode('default', done, done); }, + setNoCheckCertMode: function(done) { cordova.plugin.http.setSSLCertMode('nocheck', done, done); }, + setPinnedCertMode: function(done) { cordova.plugin.http.setSSLCertMode('pinned', done, done); }, setJsonSerializer: function(done) { done(cordova.plugin.http.setDataSerializer('json')); }, setUtf8StringSerializer: function(done) { done(cordova.plugin.http.setDataSerializer('utf8')); }, setUrlEncodedSerializer: function(done) { done(cordova.plugin.http.setDataSerializer('urlencoded')); }, @@ -82,7 +81,7 @@ const tests = [ },{ description: 'should accept bad cert (GET)', expected: 'resolved: {"status":200, ...', - before: helpers.acceptAllCerts, + before: helpers.setNoCheckCertMode, func: function(resolve, reject) { cordova.plugin.http.get('https://self-signed.badssl.com/', {}, {}, resolve, reject); }, validationFunc: function(driver, result) { result.type.should.be.equal('resolved'); @@ -91,7 +90,7 @@ const tests = [ },{ description: 'should accept bad cert (PUT)', expected: 'rejected: {"status":405, ... // will be rejected because PUT is not allowed', - before: helpers.acceptAllCerts, + before: helpers.setNoCheckCertMode, func: function(resolve, reject) { cordova.plugin.http.put('https://self-signed.badssl.com/', { test: 'testString' }, {}, resolve, reject); }, validationFunc: function(driver, result) { result.type.should.be.equal('rejected'); @@ -100,7 +99,7 @@ const tests = [ },{ description: 'should accept bad cert (POST)', expected: 'rejected: {"status":405, ... // will be rejected because POST is not allowed', - before: helpers.acceptAllCerts, + before: helpers.setNoCheckCertMode, func: function(resolve, reject) { cordova.plugin.http.post('https://self-signed.badssl.com/', { test: 'testString' }, {}, resolve, reject); }, validationFunc: function(driver, result) { result.type.should.be.equal('rejected'); @@ -109,7 +108,7 @@ const tests = [ },{ description: 'should accept bad cert (PATCH)', expected: 'rejected: {"status":405, ... // will be rejected because PATCH is not allowed', - before: helpers.acceptAllCerts, + before: helpers.setNoCheckCertMode, func: function(resolve, reject) { cordova.plugin.http.patch('https://self-signed.badssl.com/', { test: 'testString' }, {}, resolve, reject); }, validationFunc: function(driver, result) { result.type.should.be.equal('rejected'); @@ -118,7 +117,7 @@ const tests = [ },{ description: 'should accept bad cert (DELETE)', expected: 'rejected: {"status":405, ... // will be rejected because DELETE is not allowed', - before: helpers.acceptAllCerts, + before: helpers.setNoCheckCertMode, func: function(resolve, reject) { cordova.plugin.http.delete('https://self-signed.badssl.com/', {}, {}, resolve, reject); }, validationFunc: function(driver, result) { result.type.should.be.equal('rejected'); @@ -127,7 +126,7 @@ const tests = [ },{ description: 'should fetch data from http://httpbin.org/ (GET)', expected: 'resolved: {"status":200, ...', - before: helpers.acceptAllCerts, + before: helpers.setNoCheckCertMode, func: function(resolve, reject) { cordova.plugin.http.get('http://httpbin.org/', {}, {}, resolve, reject); }, validationFunc: function(driver, result) { result.type.should.be.equal('resolved'); @@ -430,7 +429,7 @@ const tests = [ },{ description: 'should pin SSL cert correctly (GET)', expected: 'resolved: {"status": 200 ...', - before: helpers.enableSSLPinning, + before: helpers.setPinnedCertMode, func: function(resolve, reject) { cordova.plugin.http.get('https://httpbin.org', {}, {}, resolve, reject); }, @@ -440,7 +439,7 @@ const tests = [ },{ description: 'should reject when pinned cert does not match received server cert (GET)', expected: 'rejected: {"status": -1 ...', - before: helpers.enableSSLPinning, + before: helpers.setPinnedCertMode, func: function(resolve, reject) { cordova.plugin.http.get('https://sha512.badssl.com/', {}, {}, resolve, reject); }, diff --git a/www/advanced-http.js b/www/advanced-http.js index 16febc6..403dd04 100644 --- a/www/advanced-http.js +++ b/www/advanced-http.js @@ -67,14 +67,11 @@ var publicInterface = { setRequestTimeout: function (timeout) { globalConfigs.timeout = timeout; }, - enableSSLPinning: function (enable, success, failure) { - return exec(success, failure, 'CordovaHttpPlugin', 'enableSSLPinning', [ enable ]); - }, - acceptAllCerts: function (allow, success, failure) { - return exec(success, failure, 'CordovaHttpPlugin', 'acceptAllCerts', [ allow ]); + setSSLCertMode: function (mode, success, failure) { + return exec(success, failure, 'CordovaHttpPlugin', 'setSSLCertMode', [ helpers.checkSSLCertMode(mode) ]); }, disableRedirect: function (disable, success, failure) { - return exec(success, failure, 'CordovaHttpPlugin', 'disableRedirect', [ disable ]); + return exec(success, failure, 'CordovaHttpPlugin', 'disableRedirect', [ !!disable ]); }, sendRequest: function (url, options, success, failure) { helpers.handleMissingCallbacks(success, failure); diff --git a/www/helpers.js b/www/helpers.js index 979d7e0..4d636b2 100644 --- a/www/helpers.js +++ b/www/helpers.js @@ -3,12 +3,14 @@ var cookieHandler = require(pluginId + '.cookie-handler'); var messages = require(pluginId + '.messages'); var validSerializers = [ 'urlencoded', 'json', 'utf8' ]; +var validCertModes = [ 'default', 'nocheck', 'pinned' ]; var validHttpMethods = [ 'get', 'put', 'post', 'patch', 'head', 'delete', 'upload', 'download' ]; module.exports = { b64EncodeUnicode: b64EncodeUnicode, getTypeOf: getTypeOf, checkSerializer: checkSerializer, + checkSSLCertMode: checkSSLCertMode, checkForBlacklistedHeaderKey: checkForBlacklistedHeaderKey, checkForInvalidHeaderValue: checkForInvalidHeaderValue, injectCookieHandler: injectCookieHandler, @@ -79,6 +81,10 @@ function checkSerializer(serializer) { return checkForValidStringValue(validSerializers, serializer, messages.INVALID_DATA_SERIALIZER); } +function checkSSLCertMode(mode) { + return checkForValidStringValue(validCertModes, mode, messages.INVALID_SSL_CERT_MODE); +} + function checkForBlacklistedHeaderKey(key) { if (key.toLowerCase() === 'cookie') { throw new Error(messages.ADDING_COOKIES_NOT_SUPPORTED); diff --git a/www/messages.js b/www/messages.js index e30afe1..da37826 100644 --- a/www/messages.js +++ b/www/messages.js @@ -5,6 +5,7 @@ module.exports = { MANDATORY_FAIL: 'advanced-http: missing mandatory "onFail" callback function', INVALID_HTTP_METHOD: 'advanced-http: invalid HTTP method, supported methods are:', INVALID_DATA_SERIALIZER: 'advanced-http: invalid serializer, supported serializers are:', + INVALID_SSL_CERT_MODE: 'advanced-http: invalid SSL cert mode, supported modes are:', INVALID_HEADERS_VALUE: 'advanced-http: header values must be strings', INVALID_TIMEOUT_VALUE: 'advanced-http: invalid timeout value, needs to be a positive numeric value', INVALID_PARAMS_VALUE: 'advanced-http: invalid params object, needs to be an object with strings'