diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 4db92eb7cbbb8a4b3a5ab67e0672ccaf86f4f03f..0edd09a9a7895f6abe18ba8132549cf595ad1241 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -3,6 +3,7 @@ stages:
   - docker_image_sdk
   - docker_image_other
   - build
+  - test
 
 # When using dind, it's wise to use the overlayfs driver for
 # improved performance.
@@ -50,6 +51,12 @@ docker_image:emulator:
     DOCKER_IMAGE: android-emulator
   <<: *build_docker_image
 
+unit_test:
+  image: "0xacab.org:4567/leap/bitmask_android/android-ndk:latest"
+  stage: test
+  script:
+    - ./gradlew test
+
 build:
   image: "0xacab.org:4567/leap/bitmask_android/android-ndk:latest"
   stage: build
diff --git a/app/assets/calyx.net.json b/app/assets/calyx.net.json
new file mode 100644
index 0000000000000000000000000000000000000000..30ab43c54170d77d26f5d791e22bc45408fb4f20
--- /dev/null
+++ b/app/assets/calyx.net.json
@@ -0,0 +1,37 @@
+{
+  "api_uri": "https://calyx.net:4430",
+  "api_version": "1",
+  "ca_cert_fingerprint": "SHA256: 43683c9ba3862c5384a8c1885072fcac40b5d2d4dd67331443f13a3077fa2e69",
+  "ca_cert_uri": "https://calyx.net/ca.crt",
+  "default_language": "en",
+  "description": {
+    "en": "Calyx Institute privacy focused ISP testbed"
+  },
+  "domain": "calyx.net",
+  "enrollment_policy": "open",
+  "languages": [
+    "en"
+  ],
+  "name": {
+    "en": "calyx"
+  },
+  "service": {
+    "allow_anonymous": false,
+    "allow_free": true,
+    "allow_limited_bandwidth": false,
+    "allow_paid": false,
+    "allow_registration": true,
+    "allow_unlimited_bandwidth": true,
+    "bandwidth_limit": 102400,
+    "default_service_level": 1,
+    "levels": {
+      "1": {
+        "description": "Please donate.",
+        "name": "free"
+      }
+    }
+  },
+  "services": [
+    "openvpn"
+  ]
+}
\ No newline at end of file
diff --git a/app/assets/calyx.net.pem b/app/assets/calyx.net.pem
new file mode 100644
index 0000000000000000000000000000000000000000..cedb2e38219fc350e014b6871a45fe65efa34e4d
--- /dev/null
+++ b/app/assets/calyx.net.pem
@@ -0,0 +1,31 @@
+-----BEGIN CERTIFICATE-----
+MIIFYzCCA0ugAwIBAgIBATANBgkqhkiG9w0BAQ0FADBEMQ4wDAYDVQQKDAVjYWx5
+eDEaMBgGA1UECwwRaHR0cHM6Ly9jYWx5eC5uZXQxFjAUBgNVBAMMDWNhbHl4IFJv
+b3QgQ0EwHhcNMTMwNzAyMDAwMDAwWhcNMjMwNzAyMDAwMDAwWjBEMQ4wDAYDVQQK
+DAVjYWx5eDEaMBgGA1UECwwRaHR0cHM6Ly9jYWx5eC5uZXQxFjAUBgNVBAMMDWNh
+bHl4IFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDupdnx
+Bgat537XOqrZOulE/RvjoXB1S07sy9/MMtksXFoQuWJZRCSTp1Jaqg3H/e9o1nct
+LQO91+izfJe07TUyajFl7CfllYgMeyKTYcT85dFwNX4pcIHZr8UpmO0MpGBoR4W1
+8cPa3vxAG0CsyUmrASJVyhRouk4qazRosM5RwBxTdMzCK7L3SwqPQoxlY9YmRJlD
+XYZlK5VMJd0dj9XxhMeFs5n43R0bsDENryrExSbuxoNfnUoQg3wffKk+Z0gW7YgW
+ivPsbObqOgXUuBEU0xr9xMNBpU33ffLIsccrHq1EKp8zGfCOcww6v7+zEadUkVLo
+6j/rRhYYgRw9lijZG1rMuV/mTGnUqbjHsdoz5mzkFFWeTSqo44lvhveUyCcwRNmi
+2sjS77l0fCTzfreufffFoOEcRVMRfsnJdu/xPeARoXILEx8nQ421mSn6spOZlDQr
+Tt0T0BAWt+VNc+m0IGSW3SwS7r5MUyQ/M5GrbQBGi5W2SzPriKZ79YTOwPVmXKLZ
+vJoEuKRDkEPJLBAhcD5oSQljOm/Wp/hjmRH4HnI1y4XMshWlDsyRDB1Au5yrsfwN
+noFVSskEcbXlZfNgml4lktLBqz+qwsw+voq6Ak7ROKbc0ii5s8+iNMbAtIK7GcFF
+kuKKIyRmmGlDim/SDhlNdWo7Ah4Akde7zfWufwIDAQABo2AwXjAdBgNVHQ4EFgQU
+AY8+K4ZupAQ+L9ttFJG3vaLBq5gwDgYDVR0PAQH/BAQDAgIEMAwGA1UdEwQFMAMB
+Af8wHwYDVR0jBBgwFoAUAY8+K4ZupAQ+L9ttFJG3vaLBq5gwDQYJKoZIhvcNAQEN
+BQADggIBAOpXi5o3g/2o2rPa53iG7Zgcy8RpePGgZk6xknGYWeLamEqSh+XWQZ2w
+2kQP54bf8HfPj3ugJBWsVtYAs/ltJwzeBfYDrwEJd1N8tw2IRuGlQOWiTAVVLBj4
+Zs+dikSuMoA399f/7BlUIEpVLUiV/emTtbkjFnDeKEV9zql6ypR0BtR8Knf8ALvL
+YfMsWLvTe4rXeypzxIaE2pn8ttcXLYAX0ml2MofTi5xcDhMn1vznKIvs82xhncQx
+I1MJMWqPHNHgJUJpA+y1IFh5LPbpag9PKQ0yQ9sM+/dyGumF2jElsMw71flh/Txr
+2dEv8+FNV1pPK26XJZBK24rNWFs30eAFfH9EQCwVla174I4PDoWqsIR7vtQMObDt
+Bq34R3TjjJJIt2sCSlYLooWwiK7Q+d/SgYqA+MSDmmwhzm86ToK6cwbCsvuw1AxR
+X6VIs4U8wOotgljzX/CSpKqlxcqZjhnAuelZ1+KiN8RHKPj7AzSLYOv/YwTjLTIq
+EOxquoNR58uDa5pBG22a7xWbSaKosn/mEl8SrUr6klzzc8Vh09IMoxrw74uLdAg2
+1jnrhm7qg91Ttb0aXiqbV+Kg/qQzojdewnnoBFnv4jaQ3y8zDCfMhsBtWlWz4Knb
+Zqga1WyRm3Gj1j6IV0oOincYMrw5YA7bgXpwop/Lo/mmliMA14ps
+-----END CERTIFICATE-----
\ No newline at end of file
diff --git a/app/assets/demo.bitmask.net.json b/app/assets/demo.bitmask.net.json
new file mode 100644
index 0000000000000000000000000000000000000000..e7fe6099a17951676f910c537734b6b3a399f356
--- /dev/null
+++ b/app/assets/demo.bitmask.net.json
@@ -0,0 +1,42 @@
+{
+  "api_uri": "https://api.demo.bitmask.net:4430",
+  "api_version": "1",
+  "ca_cert_fingerprint": "SHA256: 0f17c033115f6b76ff67871872303ff65034efe7dd1b910062ca323eb4da5c7e",
+  "ca_cert_uri": "https://demo.bitmask.net/ca.crt",
+  "default_language": "en",
+  "description": {
+    "el": "demo.bitmask.net allows you to test the Bitmask application. User accounts may be periodically deleted.",
+    "en": "demo.bitmask.net allows you to test the Bitmask application. User accounts may be periodically deleted.",
+    "es": "demo.bitmask.net allows you to test the Bitmask application. User accounts may be periodically deleted."
+  },
+  "domain": "demo.bitmask.net",
+  "enrollment_policy": "open",
+  "languages": [
+    "de",
+    "en",
+    "es",
+    "pt"
+  ],
+  "name": {
+    "en": "Bitmask"
+  },
+  "service": {
+    "allow_anonymous": true,
+    "allow_free": true,
+    "allow_limited_bandwidth": false,
+    "allow_paid": false,
+    "allow_registration": true,
+    "allow_unlimited_bandwidth": true,
+    "bandwidth_limit": 102400,
+    "default_service_level": 1,
+    "levels": {
+      "1": {
+        "description": "Please donate.",
+        "name": "free"
+      }
+    }
+  },
+  "services": [
+    "openvpn"
+  ]
+}
\ No newline at end of file
diff --git a/app/assets/demo.bitmask.net.pem b/app/assets/demo.bitmask.net.pem
new file mode 100644
index 0000000000000000000000000000000000000000..9a4221613799b05db8316f40077a525050058e99
--- /dev/null
+++ b/app/assets/demo.bitmask.net.pem
@@ -0,0 +1,32 @@
+-----BEGIN CERTIFICATE-----
+MIIFbzCCA1egAwIBAgIBATANBgkqhkiG9w0BAQ0FADBKMRgwFgYDVQQDDA9CaXRt
+YXNrIFJvb3QgQ0ExEDAOBgNVBAoMB0JpdG1hc2sxHDAaBgNVBAsME2h0dHBzOi8v
+Yml0bWFzay5uZXQwHhcNMTIxMTA2MDAwMDAwWhcNMjIxMTA2MDAwMDAwWjBKMRgw
+FgYDVQQDDA9CaXRtYXNrIFJvb3QgQ0ExEDAOBgNVBAoMB0JpdG1hc2sxHDAaBgNV
+BAsME2h0dHBzOi8vYml0bWFzay5uZXQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw
+ggIKAoICAQC1eV4YvayaU+maJbWrD4OHo3d7S1BtDlcvkIRS1Fw3iYDjsyDkZxai
+dHp4EUasfNQ+EVtXUvtk6170EmLco6Elg8SJBQ27trE6nielPRPCfX3fQzETRfvB
+7tNvGw4Jn2YKiYoMD79kkjgyZjkJ2r/bEHUSevmR09BRp86syHZerdNGpXYhcQ84
+CA1+V+603GFIHnrP+uQDdssW93rgDNYu+exT+Wj6STfnUkugyjmPRPjL7wh0tzy+
+znCeLl4xiV3g9sjPnc7r2EQKd5uaTe3j71sDPF92KRk0SSUndREz+B1+Dbe/RGk4
+MEqGFuOzrtsgEhPIX0hplhb0Tgz/rtug+yTT7oJjBa3u20AAOQ38/M99EfdeJvc4
+lPFF1XBBLh6X9UKF72an2NuANiX6XPySnJgZ7nZ09RiYZqVwu/qt3DfvLfhboq+0
+bQvLUPXrVDr70onv5UDjpmEA/cLmaIqqrduuTkFZOym65/PfAPvpGnt7crQj/Ibl
+DEDYZQmP7AS+6zBjoOzNjUGE5r40zWAR1RSi7zliXTu+yfsjXUIhUAWmYR6J3KxB
+lfsiHBQ+8dn9kC3YrUexWoOqBiqJOAJzZh5Y1tqgzfh+2nmHSB2dsQRs7rDRRlyy
+YMbkpzL9ZsOUO2eTP1mmar6YjCN+rggYjRrX71K2SpBG6b1zZxOG+wIDAQABo2Aw
+XjAdBgNVHQ4EFgQUuYGDLL2sswnYpHHvProt1JU+D48wDgYDVR0PAQH/BAQDAgIE
+MAwGA1UdEwQFMAMBAf8wHwYDVR0jBBgwFoAUuYGDLL2sswnYpHHvProt1JU+D48w
+DQYJKoZIhvcNAQENBQADggIBADeG67vaFcbITGpi51264kHPYPEWaXUa5XYbtmBl
+cXYyB6hY5hv/YNuVGJ1gWsDmdeXEyj0j2icGQjYdHRfwhrbEri+h1EZOm1cSBDuY
+k/P5+ctHyOXx8IE79DBsZ6IL61UKIaKhqZBfLGYcWu17DVV6+LT+AKtHhOrv3TSj
+RnAcKnCbKqXLhUPXpK0eTjPYS2zQGQGIhIy9sQXVXJJJsGrPgMxna1Xw2JikBOCG
+htD/JKwt6xBmNwktH0GI/LVtVgSp82Clbn9C4eZN9E5YbVYjLkIEDhpByeC71QhX
+EIQ0ZR56bFuJA/CwValBqV/G9gscTPQqd+iETp8yrFpAVHOW+YzSFbxjTEkBte1J
+aF0vmbqdMAWLk+LEFPQRptZh0B88igtx6tV5oVd+p5IVRM49poLhuPNJGPvMj99l
+mlZ4+AeRUnbOOeAEuvpLJbel4rhwFzmUiGoeTVoPZyMevWcVFq6BMkS+jRR2w0jK
+G6b0v5XDHlcFYPOgUrtsOBFJVwbutLvxdk6q37kIFnWCd8L3kmES5q4wjyFK47Co
+Ja8zlx64jmMZPg/t3wWqkZgXZ14qnbyG5/lGsj5CwVtfDljrhN0oCWK1FZaUmW3d
+69db12/g4f6phldhxiWuGC/W6fCW5kre7nmhshcltqAJJuU47iX+DarBFiIj816e
+yV8e
+-----END CERTIFICATE-----
\ No newline at end of file
diff --git a/app/assets/riseup.net.json b/app/assets/riseup.net.json
new file mode 100644
index 0000000000000000000000000000000000000000..9a5ec79e24d8ed16451262dab304a94fa97a6e89
--- /dev/null
+++ b/app/assets/riseup.net.json
@@ -0,0 +1,37 @@
+{
+  "api_uri": "https://api.black.riseup.net:443",
+  "api_version": "1",
+  "ca_cert_fingerprint": "SHA256: a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494",
+  "ca_cert_uri": "https://black.riseup.net/ca.crt",
+  "default_language": "en",
+  "description": {
+    "en": "Riseup is a non-profit collective in Seattle that provides online communication tools for people and groups working toward liberatory social change."
+  },
+  "domain": "riseup.net",
+  "enrollment_policy": "open",
+  "languages": [
+    "en"
+  ],
+  "name": {
+    "en": "Riseup Networks"
+  },
+  "service": {
+    "allow_anonymous": false,
+    "allow_free": true,
+    "allow_limited_bandwidth": false,
+    "allow_paid": false,
+    "allow_registration": true,
+    "allow_unlimited_bandwidth": true,
+    "bandwidth_limit": 102400,
+    "default_service_level": 1,
+    "levels": {
+      "1": {
+        "description": "Please donate.",
+        "name": "free"
+      }
+    }
+  },
+  "services": [
+    "openvpn"
+  ]
+}
\ No newline at end of file
diff --git a/app/assets/riseup.net.pem b/app/assets/riseup.net.pem
new file mode 100644
index 0000000000000000000000000000000000000000..c890aff40d14cfaf7f0dc475322e5592c9274f0a
--- /dev/null
+++ b/app/assets/riseup.net.pem
@@ -0,0 +1,32 @@
+-----BEGIN CERTIFICATE-----
+MIIFjTCCA3WgAwIBAgIBATANBgkqhkiG9w0BAQ0FADBZMRgwFgYDVQQKDA9SaXNl
+dXAgTmV0d29ya3MxGzAZBgNVBAsMEmh0dHBzOi8vcmlzZXVwLm5ldDEgMB4GA1UE
+AwwXUmlzZXVwIE5ldHdvcmtzIFJvb3QgQ0EwHhcNMTQwNDI4MDAwMDAwWhcNMjQw
+NDI4MDAwMDAwWjBZMRgwFgYDVQQKDA9SaXNldXAgTmV0d29ya3MxGzAZBgNVBAsM
+Emh0dHBzOi8vcmlzZXVwLm5ldDEgMB4GA1UEAwwXUmlzZXVwIE5ldHdvcmtzIFJv
+b3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC76J4ciMJ8Sg0m
+TP7DF2DT9zNe0Csk4myoMFC57rfJeqsAlJCv1XMzBmXrw8wq/9z7XHv6n/0sWU7a
+7cF2hLR33ktjwODlx7vorU39/lXLndo492ZBhXQtG1INMShyv+nlmzO6GT7ESfNE
+LliFitEzwIegpMqxCIHXFuobGSCWF4N0qLHkq/SYUMoOJ96O3hmPSl1kFDRMtWXY
+iw1SEKjUvpyDJpVs3NGxeLCaA7bAWhDY5s5Yb2fA1o8ICAqhowurowJpW7n5ZuLK
+5VNTlNy6nZpkjt1QycYvNycffyPOFm/Q/RKDlvnorJIrihPkyniV3YY5cGgP+Qkx
+HUOT0uLA6LHtzfiyaOqkXwc4b0ZcQD5Vbf6Prd20Ppt6ei0zazkUPwxld3hgyw58
+m/4UIjG3PInWTNf293GngK2Bnz8Qx9e/6TueMSAn/3JBLem56E0WtmbLVjvko+LF
+PM5xA+m0BmuSJtrD1MUCXMhqYTtiOvgLBlUm5zkNxALzG+cXB28k6XikXt6MRG7q
+hzIPG38zwkooM55yy5i1YfcIi5NjMH6A+t4IJxxwb67MSb6UFOwg5kFokdONZcwj
+shczHdG9gLKSBIvrKa03Nd3W2dF9hMbRu//STcQxOailDBQCnXXfAATj9pYzdY4k
+ha8VCAREGAKTDAex9oXf1yRuktES4QIDAQABo2AwXjAdBgNVHQ4EFgQUC4tdmLVu
+f9hwfK4AGliaet5KkcgwDgYDVR0PAQH/BAQDAgIEMAwGA1UdEwQFMAMBAf8wHwYD
+VR0jBBgwFoAUC4tdmLVuf9hwfK4AGliaet5KkcgwDQYJKoZIhvcNAQENBQADggIB
+AGzL+GRnYu99zFoy0bXJKOGCF5XUXP/3gIXPRDqQf5g7Cu/jYMID9dB3No4Zmf7v
+qHjiSXiS8jx1j/6/Luk6PpFbT7QYm4QLs1f4BlfZOti2KE8r7KRDPIecUsUXW6P/
+3GJAVYH/+7OjA39za9AieM7+H5BELGccGrM5wfl7JeEz8in+V2ZWDzHQO4hMkiTQ
+4ZckuaL201F68YpiItBNnJ9N5nHr1MRiGyApHmLXY/wvlrOpclh95qn+lG6/2jk7
+3AmihLOKYMlPwPakJg4PYczm3icFLgTpjV5sq2md9bRyAg3oPGfAuWHmKj2Ikqch
+Td5CHKGxEEWbGUWEMP0s1A/JHWiCbDigc4Cfxhy56CWG4q0tYtnc2GMw8OAUO6Wf
+Xu5pYKNkzKSEtT/MrNJt44tTZWbKV/Pi/N2Fx36my7TgTUj7g3xcE9eF4JV2H/sg
+tsK3pwE0FEqGnT4qMFbixQmc8bGyuakr23wjMvfO7eZUxBuWYR2SkcP26sozF9PF
+tGhbZHQVGZUTVPyvwahMUEhbPGVerOW0IYpxkm0x/eaWdTc4vPpf/rIlgbAjarnJ
+UN9SaWRlWKSdP4haujnzCoJbM7dU9bjvlGZNyXEekgeT0W2qFeGGp+yyUWw8tNsp
+0BuC1b7uW/bBn/xKm319wXVDvBgZgcktMolak39V7DVO
+-----END CERTIFICATE-----
\ No newline at end of file
diff --git a/app/assets/urls/calyx.url b/app/assets/urls/calyx.net.url
similarity index 100%
rename from app/assets/urls/calyx.url
rename to app/assets/urls/calyx.net.url
diff --git a/app/assets/urls/bitmask demo.url b/app/assets/urls/demo.bitmask.net.url
similarity index 100%
rename from app/assets/urls/bitmask demo.url
rename to app/assets/urls/demo.bitmask.net.url
diff --git a/app/assets/urls/riseup.url b/app/assets/urls/riseup.net.url
similarity index 100%
rename from app/assets/urls/riseup.url
rename to app/assets/urls/riseup.net.url
diff --git a/app/build.gradle b/app/build.gradle
index 048eb597d6b5ce2244b0e46a69d0183aeb7589ab..52c64ab73ed622132682e2b4d8c2c7bd3d7e890b 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -64,7 +64,16 @@ android {
 }
 
 dependencies {
-  testCompile 'org.mockito:mockito-core:2.6.3'
+  testCompile 'junit:junit:4.12'
+  testCompile 'org.mockito:mockito-core:2.8.0'
+  testCompile 'org.powermock:powermock-api-mockito2:1.7.3'
+  testCompile 'org.powermock:powermock-module-junit4:1.7.3'
+  testCompile 'org.powermock:powermock-core:1.7.3'
+  testCompile 'org.powermock:powermock-module-junit4-rule:1.7.3'
+  //testCompile 'com.madgag.spongycastle:bctls-jdk15on:1.58.0.0'
+  //testCompile 'com.madgag.spongycastle:bcpkix-jdk15on:1.58.0.0'
+  //testCompile 'com.madgag.spongycastle:bcpg-jdk15on:1.58.0.0'
+
   androidTestCompile 'com.jayway.android.robotium:robotium-solo:5.6.3'
   testCompile 'junit:junit:4.12'
   testCompile 'org.json:json:20170516'
@@ -72,8 +81,7 @@ dependencies {
   provided 'com.squareup.dagger:dagger-compiler:1.2.2'
   compile 'com.github.pedrovgs:renderers:1.5'
   compile 'com.intellij:annotations:12.0'
-  compile 'com.google.code.gson:gson:2.4'
-  compile 'org.thoughtcrime.ssl.pinning:AndroidPinning:1.0.0'
+  compile 'com.google.code.gson:gson:2.8.2'
   compile 'com.squareup.okhttp3:okhttp:3.9.0'
   compile 'mbanje.kurt:fabbutton:1.1.4'
   compile "com.android.support:support-core-utils:26.1.0"
diff --git a/app/src/androidTest/java/se/leap/bitmaskclient/test/TestEIP.java b/app/src/androidTest/java/se/leap/bitmaskclient/test/TestEIP.java
index c5a6e809aae42caee1a5d0f33df605897fc9dc62..fc763c7d2ee3f248e1e89fed387370f381c4b82a 100644
--- a/app/src/androidTest/java/se/leap/bitmaskclient/test/TestEIP.java
+++ b/app/src/androidTest/java/se/leap/bitmaskclient/test/TestEIP.java
@@ -37,7 +37,7 @@ public class TestEIP extends ServiceTestCase<EIP> {
         super(activityClass);
         context = getSystemContext();
         intent = new Intent(context, EIP.class);
-        preferences = context.getSharedPreferences(Dashboard.SHARED_PREFERENCES, Context.MODE_PRIVATE);
+        preferences = context.getSharedPreferences(Constants.SHARED_PREFERENCES, Context.MODE_PRIVATE);
     }
 
     @Override
diff --git a/app/src/insecure/java/se/leap/bitmaskclient/ConfigurationWizard.java b/app/src/insecure/java/se/leap/bitmaskclient/ConfigurationWizard.java
index df1a59ffabee81e2a219a10e899f7b34493fede3..766b6c606a071e69d0bd7e4f147dd6128f337f23 100644
--- a/app/src/insecure/java/se/leap/bitmaskclient/ConfigurationWizard.java
+++ b/app/src/insecure/java/se/leap/bitmaskclient/ConfigurationWizard.java
@@ -89,9 +89,17 @@ public class ConfigurationWizard extends BaseConfigurationWizard {
         mConfigState.setAction(SETTING_UP_PROVIDER);
         Intent provider_API_command = new Intent(this, ProviderAPI.class);
         Bundle parameters = new Bundle();
-        parameters.putString(Provider.MAIN_URL, selected_provider.mainUrl().getUrl().toString());
+        parameters.putString(Provider.MAIN_URL, selected_provider.getMainUrl().toString());
         parameters.putBoolean(ProviderItem.DANGER_ON, danger_on);
-        parameters.putString(Provider.CA_CERT_FINGERPRINT, selected_provider.certificatePin());
+        if (selected_provider.hasCertificatePin()){
+            parameters.putString(Provider.CA_CERT_FINGERPRINT, selected_provider.certificatePin());
+        }
+        if (selected_provider.hasCaCert()) {
+            parameters.putString(Provider.CA_CERT, selected_provider.getCaCert());
+        }
+        if (selected_provider.hasDefinition()) {
+            parameters.putString(Provider.KEY, selected_provider.getDefinition().toString());
+        }
 
         provider_API_command.setAction(ProviderAPI.SET_UP_PROVIDER);
         provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters);
@@ -103,15 +111,22 @@ public class ConfigurationWizard extends BaseConfigurationWizard {
     /**
      * Retrys setup of last used provider, allows bypassing ca certificate validation.
      */
+    @Override
     public void retrySetUpProvider() {
         cancelSettingUpProvider();
         if (!ProviderAPI.caCertDownloaded()) {
             addAndSelectNewProvider(ProviderAPI.lastProviderMainUrl(), ProviderAPI.lastDangerOn());
         } else {
+            showProgressBar();
+            adapter.hideAllBut(adapter.indexOf(selected_provider));
+
             Intent provider_API_command = new Intent(this, ProviderAPI.class);
 
             provider_API_command.setAction(ProviderAPI.SET_UP_PROVIDER);
             provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, providerAPI_result_receiver);
+            Bundle parameters = new Bundle();
+            parameters.putString(Provider.MAIN_URL, selected_provider.getMainUrl().toString());
+            provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters);
 
             startService(provider_API_command);
         }
diff --git a/app/src/insecure/java/se/leap/bitmaskclient/ProviderAPI.java b/app/src/insecure/java/se/leap/bitmaskclient/ProviderApiManager.java
similarity index 61%
rename from app/src/insecure/java/se/leap/bitmaskclient/ProviderAPI.java
rename to app/src/insecure/java/se/leap/bitmaskclient/ProviderApiManager.java
index 7689c34368cbf88610425a95adbb1565039ddbd3..6105c4b79cd43693e254a7d710c087e815776168 100644
--- a/app/src/insecure/java/se/leap/bitmaskclient/ProviderAPI.java
+++ b/app/src/insecure/java/se/leap/bitmaskclient/ProviderApiManager.java
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2013 LEAP Encryption Access Project and contributers
+ * Copyright (c) 2018 LEAP Encryption Access Project and contributers
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -14,14 +14,16 @@
  * You should have received a copy of the GNU General Public License
  * along with this program. If not, see <http://www.gnu.org/licenses/>.
  */
+
 package se.leap.bitmaskclient;
 
+import android.content.SharedPreferences;
+import android.content.res.Resources;
 import android.os.Bundle;
 import android.util.Pair;
 
 import org.json.JSONException;
 import org.json.JSONObject;
-import org.thoughtcrime.ssl.pinning.util.PinningHelper;
 
 import java.io.FileNotFoundException;
 import java.io.IOException;
@@ -38,36 +40,35 @@ import javax.net.ssl.HostnameVerifier;
 import javax.net.ssl.HttpsURLConnection;
 import javax.net.ssl.KeyManager;
 import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLHandshakeException;
 import javax.net.ssl.SSLSession;
 import javax.net.ssl.TrustManager;
 import javax.net.ssl.X509TrustManager;
 
 import okhttp3.OkHttpClient;
-import se.leap.bitmaskclient.ProviderListContent.ProviderItem;
 import se.leap.bitmaskclient.eip.EIP;
 
+import static android.text.TextUtils.isEmpty;
+import static se.leap.bitmaskclient.DownloadFailedDialog.DOWNLOAD_ERRORS.ERROR_CERTIFICATE_PINNING;
+import static se.leap.bitmaskclient.ProviderAPI.ERRORS;
+import static se.leap.bitmaskclient.ProviderAPI.RESULT_KEY;
 import static se.leap.bitmaskclient.R.string.certificate_error;
-import static se.leap.bitmaskclient.R.string.error_io_exception_user_message;
 import static se.leap.bitmaskclient.R.string.malformed_url;
+import static se.leap.bitmaskclient.R.string.warning_corrupted_provider_cert;
 
 /**
- * Implements HTTP api methods used to manage communications with the provider server.
- * It extends the abstract ProviderApiBase and implements the diverging method calls between the different flavors
- * of ProviderAPI.
- * <p/>
- * It extends an  IntentService because it downloads data from the Internet, so it operates in the background.
- *
- * @author parmegv
- * @author MeanderingCode
- * @author cyberta
+ * Created by cyberta on 04.01.18.
  */
-public class ProviderAPI extends ProviderApiBase {
 
-    private static boolean last_danger_on = true;
+public class ProviderApiManager extends ProviderApiManagerBase {
+    protected static boolean lastDangerOn = true;
+
+
+    public ProviderApiManager(SharedPreferences preferences, Resources resources, OkHttpClientGenerator clientGenerator, ProviderApiServiceCallback callback) {
+        super(preferences, resources, clientGenerator, callback);
+    }
 
     public static boolean lastDangerOn() {
-        return last_danger_on;
+        return lastDangerOn;
     }
 
     /**
@@ -79,71 +80,105 @@ public class ProviderAPI extends ProviderApiBase {
     @Override
     protected Bundle setUpProvider(Bundle task) {
         int progress = 0;
-        Bundle current_download = new Bundle();
+        Bundle currentDownload = new Bundle();
 
         if (task != null) {
-            last_danger_on = task.containsKey(ProviderItem.DANGER_ON) && task.getBoolean(ProviderItem.DANGER_ON);
-            last_provider_main_url = task.containsKey(Provider.MAIN_URL) ?
+            lastDangerOn = task.containsKey(ProviderListContent.ProviderItem.DANGER_ON) && task.getBoolean(ProviderListContent.ProviderItem.DANGER_ON);
+            lastProviderMainUrl = task.containsKey(Provider.MAIN_URL) ?
                     task.getString(Provider.MAIN_URL) :
                     "";
-            provider_ca_cert_fingerprint = task.containsKey(Provider.CA_CERT_FINGERPRINT) ?
+
+            if (isEmpty(lastProviderMainUrl)) {
+                setErrorResult(currentDownload, malformed_url, null);
+                return currentDownload;
+            }
+
+            providerCaCertFingerprint = task.containsKey(Provider.CA_CERT_FINGERPRINT) ?
                     task.getString(Provider.CA_CERT_FINGERPRINT) :
                     "";
-            CA_CERT_DOWNLOADED = PROVIDER_JSON_DOWNLOADED = EIP_SERVICE_JSON_DOWNLOADED = false;
+            providerCaCert = task.containsKey(Provider.CA_CERT) ?
+                    task.getString(Provider.CA_CERT) :
+                    "";
+
+            try {
+                providerDefinition = task.containsKey(Provider.KEY) ?
+                        new JSONObject(task.getString(Provider.KEY)) :
+                        new JSONObject();
+            } catch (JSONException e) {
+                e.printStackTrace();
+                providerDefinition = new JSONObject();
+            }
+            providerApiUrl = getApiUrlWithVersion(providerDefinition);
+
+            checkPersistedProviderUpdates();
+            currentDownload = validateProviderDetails();
+
+            //provider details invalid
+            if (currentDownload.containsKey(ERRORS)) {
+                return currentDownload;
+            }
+
+            //no provider certificate available
+            if (currentDownload.containsKey(RESULT_KEY) && !currentDownload.getBoolean(RESULT_KEY)) {
+                resetProviderDetails();
+            }
+
+            EIP_SERVICE_JSON_DOWNLOADED = false;
             go_ahead = true;
         }
 
         if (!PROVIDER_JSON_DOWNLOADED)
-            current_download = getAndSetProviderJson(last_provider_main_url, last_danger_on, provider_ca_cert_fingerprint);
-        if (PROVIDER_JSON_DOWNLOADED || (current_download.containsKey(RESULT_KEY) && current_download.getBoolean(RESULT_KEY))) {
+            currentDownload = getAndSetProviderJson(lastProviderMainUrl, lastDangerOn, providerCaCert, providerDefinition);
+        if (PROVIDER_JSON_DOWNLOADED || (currentDownload.containsKey(RESULT_KEY) && currentDownload.getBoolean(RESULT_KEY))) {
             broadcastProgress(progress++);
             PROVIDER_JSON_DOWNLOADED = true;
-            current_download = downloadCACert(last_danger_on);
 
-            if (CA_CERT_DOWNLOADED || (current_download.containsKey(RESULT_KEY) && current_download.getBoolean(RESULT_KEY))) {
+            if (!CA_CERT_DOWNLOADED)
+                currentDownload = downloadCACert(lastDangerOn);
+            if (CA_CERT_DOWNLOADED || (currentDownload.containsKey(RESULT_KEY) && currentDownload.getBoolean(RESULT_KEY))) {
                 broadcastProgress(progress++);
                 CA_CERT_DOWNLOADED = true;
-                current_download = getAndSetEipServiceJson();
-                if (current_download.containsKey(RESULT_KEY) && current_download.getBoolean(RESULT_KEY)) {
+                currentDownload = getAndSetEipServiceJson();
+                if (currentDownload.containsKey(RESULT_KEY) && currentDownload.getBoolean(RESULT_KEY)) {
                     broadcastProgress(progress++);
                     EIP_SERVICE_JSON_DOWNLOADED = true;
                 }
             }
         }
 
-        return current_download;
+        return currentDownload;
     }
 
-    private Bundle getAndSetProviderJson(String provider_main_url, boolean danger_on, String provider_ca_cert_fingerprint) {
+    private Bundle getAndSetProviderJson(String providerMainUrl, boolean dangerOn, String caCert, JSONObject providerDefinition) {
         Bundle result = new Bundle();
 
         if (go_ahead) {
-            String provider_dot_json_string;
-            if(provider_ca_cert_fingerprint.isEmpty())
-                provider_dot_json_string = downloadWithCommercialCA(provider_main_url + "/provider.json", danger_on);
+            String providerDotJsonString;
+            if(providerDefinition.length() == 0 || caCert.isEmpty())
+                providerDotJsonString = downloadWithCommercialCA(providerMainUrl + "/provider.json", dangerOn);
             else
-                provider_dot_json_string = downloadWithCommercialCA(provider_main_url + "/provider.json", danger_on, provider_ca_cert_fingerprint);
+                providerDotJsonString = downloadFromApiUrlWithProviderCA("/provider.json", caCert, providerDefinition, dangerOn);
 
-            if (!isValidJson(provider_dot_json_string)) {
-                result.putString(ERRORS, getString(malformed_url));
+            if (!isValidJson(providerDotJsonString)) {
+                result.putString(ERRORS, resources.getString(malformed_url));
                 result.putBoolean(RESULT_KEY, false);
                 return result;
             }
 
             try {
-                JSONObject provider_json = new JSONObject(provider_dot_json_string);
-                provider_api_url = provider_json.getString(Provider.API_URL) + "/" + provider_json.getString(Provider.API_VERSION);
-                String name = provider_json.getString(Provider.NAME);
+                JSONObject providerJson = new JSONObject(providerDotJsonString);
+                String providerDomain = getDomainFromMainURL(lastProviderMainUrl);
+                providerApiUrl = getApiUrlWithVersion(providerJson);
+                //String name = providerJson.getString(Provider.NAME);
                 //TODO setProviderName(name);
 
-                preferences.edit().putString(Provider.KEY, provider_json.toString()).commit();
-                preferences.edit().putBoolean(Constants.PROVIDER_ALLOW_ANONYMOUS, provider_json.getJSONObject(Provider.SERVICE).getBoolean(Constants.PROVIDER_ALLOW_ANONYMOUS)).commit();
-                preferences.edit().putBoolean(Constants.PROVIDER_ALLOWED_REGISTERED, provider_json.getJSONObject(Provider.SERVICE).getBoolean(Constants.PROVIDER_ALLOWED_REGISTERED)).commit();
-
+                preferences.edit().putString(Provider.KEY, providerJson.toString()).
+                        putBoolean(Constants.PROVIDER_ALLOW_ANONYMOUS, providerJson.getJSONObject(Provider.SERVICE).getBoolean(Constants.PROVIDER_ALLOW_ANONYMOUS)).
+                        putBoolean(Constants.PROVIDER_ALLOWED_REGISTERED, providerJson.getJSONObject(Provider.SERVICE).getBoolean(Constants.PROVIDER_ALLOWED_REGISTERED)).
+                        putString(Provider.KEY + "." + providerDomain, providerJson.toString()).commit();
                 result.putBoolean(RESULT_KEY, true);
             } catch (JSONException e) {
-                //TODO Error message should be contained in that provider_dot_json_string
-                String reason_to_fail = pickErrorMessage(provider_dot_json_string);
+                String reason_to_fail = pickErrorMessage(providerDotJsonString);
                 result.putString(ERRORS, reason_to_fail);
                 result.putBoolean(RESULT_KEY, false);
             }
@@ -163,7 +198,7 @@ public class ProviderAPI extends ProviderApiBase {
             try {
                 JSONObject provider_json = new JSONObject(preferences.getString(Provider.KEY, ""));
                 String eip_service_url = provider_json.getString(Provider.API_URL) + "/" + provider_json.getString(Provider.API_VERSION) + "/" + EIP.SERVICE_API_PATH;
-                eip_service_json_string = downloadWithProviderCA(eip_service_url, last_danger_on);
+                eip_service_json_string = downloadWithProviderCA(eip_service_url, lastDangerOn);
                 JSONObject eip_service_json = new JSONObject(eip_service_json_string);
                 eip_service_json.getInt(Provider.API_RETURN_SERIAL);
 
@@ -192,7 +227,7 @@ public class ProviderAPI extends ProviderApiBase {
             String provider_main_url = provider_json.getString(Provider.API_URL);
             URL new_cert_string_url = new URL(provider_main_url + "/" + provider_json.getString(Provider.API_VERSION) + "/" + Constants.PROVIDER_VPN_CERTIFICATE);
 
-            String cert_string = downloadWithProviderCA(new_cert_string_url.toString(), last_danger_on);
+            String cert_string = downloadWithProviderCA(new_cert_string_url.toString(), lastDangerOn);
 
             if (cert_string == null || cert_string.isEmpty() || ConfigHelper.checkErroneousDownload(cert_string))
                 return false;
@@ -210,48 +245,24 @@ public class ProviderAPI extends ProviderApiBase {
     }
 
 
-    private Bundle downloadCACert(boolean danger_on) {
+    private Bundle downloadCACert(boolean dangerOn) {
         Bundle result = new Bundle();
         try {
-            JSONObject provider_json = new JSONObject(preferences.getString(Provider.KEY, ""));
-            String ca_cert_url = provider_json.getString(Provider.CA_CERT_URI);
-            String cert_string = downloadWithCommercialCA(ca_cert_url, danger_on);
+            JSONObject providerJson = new JSONObject(preferences.getString(Provider.KEY, ""));
+            String caCertUrl = providerJson.getString(Provider.CA_CERT_URI);
+            String providerDomain = providerJson.getString(Provider.DOMAIN);
+
+            String certString = downloadWithCommercialCA(caCertUrl, dangerOn);
 
-            if (validCertificate(cert_string) && go_ahead) {
-                preferences.edit().putString(Provider.CA_CERT, cert_string).commit();
+            if (validCertificate(certString) && go_ahead) {
+                preferences.edit().putString(Provider.CA_CERT, certString).commit();
+                preferences.edit().putString(Provider.CA_CERT + "." + providerDomain, certString).commit();
                 result.putBoolean(RESULT_KEY, true);
             } else {
-                String reason_to_fail = pickErrorMessage(cert_string);
-                result.putString(ERRORS, reason_to_fail);
-                result.putBoolean(RESULT_KEY, false);
+                setErrorResult(result, warning_corrupted_provider_cert, ERROR_CERTIFICATE_PINNING.toString());
             }
         } catch (JSONException e) {
-            String reason_to_fail = formatErrorMessage(malformed_url);
-            result.putString(ERRORS, reason_to_fail);
-            result.putBoolean(RESULT_KEY, false);
-        }
-
-        return result;
-    }
-
-    //TODO: refactor with ticket #8773
-    private String downloadWithCommercialCA(String urlString, boolean dangerOn, String caCertFingerprint) {
-        String result = "";
-        int seconds_of_timeout = 2;
-        String[] pins = new String[] {caCertFingerprint};
-        try {
-            URL url = new URL(urlString);
-            HttpsURLConnection connection = PinningHelper.getPinnedHttpsURLConnection(getApplicationContext(), pins, url);
-            connection.setConnectTimeout(seconds_of_timeout * 1000);
-            if (!LeapSRPSession.getToken().isEmpty())
-                connection.addRequestProperty(LeapSRPSession.AUTHORIZATION_HEADER, "Token token=" + LeapSRPSession.getToken());
-            result = new Scanner(connection.getInputStream()).useDelimiter("\\A").next();
-        } catch (IOException e) {
-            if(e instanceof SSLHandshakeException) {
-                result = dangerOn ? downloadWithoutCA(urlString) :
-                        formatErrorMessage(R.string.error_security_pinnedcertificate);
-            } else
-                result = formatErrorMessage(error_io_exception_user_message);
+            setErrorResult(result, malformed_url, null);
         }
 
         return result;
@@ -270,7 +281,7 @@ public class ProviderAPI extends ProviderApiBase {
         String responseString;
         JSONObject errorJson = new JSONObject();
 
-        OkHttpClient okHttpClient = initCommercialCAHttpClient(errorJson);
+        OkHttpClient okHttpClient = clientGenerator.initCommercialCAHttpClient(errorJson);
         if (okHttpClient == null) {
             return errorJson.toString();
         }
@@ -283,8 +294,36 @@ public class ProviderAPI extends ProviderApiBase {
             try {
                 // try to download with provider CA on certificate error
                 JSONObject responseErrorJson = new JSONObject(responseString);
-                if (danger_on && responseErrorJson.getString(ERRORS).equals(getString(R.string.certificate_error))) {
-                    responseString = downloadWithProviderCA(string_url, danger_on);
+                if (danger_on && responseErrorJson.getString(ERRORS).equals(resources.getString(R.string.certificate_error))) {
+                    responseString = downloadWithoutCA(string_url);
+                }
+            } catch (JSONException e) {
+                e.printStackTrace();
+            }
+        }
+
+        return responseString;
+    }
+
+    private String downloadFromApiUrlWithProviderCA(String path, String caCert, JSONObject providerDefinition, boolean dangerOn) {
+        String responseString;
+        JSONObject errorJson = new JSONObject();
+        String baseUrl = getApiUrl(providerDefinition);
+        OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(errorJson, caCert);
+        if (okHttpClient == null) {
+            return errorJson.toString();
+        }
+
+        String urlString = baseUrl + path;
+        List<Pair<String, String>> headerArgs = getAuthorizationHeader();
+        responseString = sendGetStringToServer(urlString, headerArgs, okHttpClient);
+
+        if (responseString != null && responseString.contains(ERRORS)) {
+            try {
+                // try to download with provider CA on certificate error
+                JSONObject responseErrorJson = new JSONObject(responseString);
+                if (dangerOn && responseErrorJson.getString(ERRORS).equals(resources.getString(R.string.certificate_error))) {
+                    responseString = downloadWithCommercialCA(urlString, dangerOn);
                 }
             } catch (JSONException e) {
                 e.printStackTrace();
@@ -305,7 +344,7 @@ public class ProviderAPI extends ProviderApiBase {
         JSONObject initError = new JSONObject();
         String responseString;
 
-        OkHttpClient okHttpClient = initSelfSignedCAHttpClient(initError);
+        OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(initError);
         if (okHttpClient == null) {
             return initError.toString();
         }
@@ -318,7 +357,7 @@ public class ProviderAPI extends ProviderApiBase {
             try {
                 // danger danger: try to download without CA on certificate error
                 JSONObject responseErrorJson = new JSONObject(responseString);
-                if (dangerOn && responseErrorJson.getString(ERRORS).equals(getString(R.string.certificate_error))) {
+                if (dangerOn && responseErrorJson.getString(ERRORS).equals(resources.getString(R.string.certificate_error))) {
                     responseString = downloadWithoutCA(urlString);
                 }
             } catch (JSONException e) {
@@ -385,5 +424,4 @@ public class ProviderAPI extends ProviderApiBase {
         }
         return string;
     }
-
 }
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4dd11143af9f549d3cdaca802263e7b80a2329e8..c0e2d90c43c16cbd01884eb0b84c5e5f62fadcf1 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -54,7 +54,7 @@
             </intent-filter>
         </service>
 	<service android:name="se.leap.bitmaskclient.ProviderAPI" android:enabled="true"/>
-	
+
         <receiver
             android:name="se.leap.bitmaskclient.OnBootReceiver"
             android:enabled="true"
diff --git a/app/src/main/assets/urls/bitmask demo.url b/app/src/main/assets/urls/bitmask demo.url
deleted file mode 100644
index 1a412055b063d0a410fad0b4429a65f6ec3aa607..0000000000000000000000000000000000000000
--- a/app/src/main/assets/urls/bitmask demo.url	
+++ /dev/null
@@ -1,3 +0,0 @@
-{
-	"main_url" : "https://demo.bitmask.net/"
-}
diff --git a/app/src/main/java/se/leap/bitmaskclient/BaseConfigurationWizard.java b/app/src/main/java/se/leap/bitmaskclient/BaseConfigurationWizard.java
index 21520dc44cffef70be8c27df26a54125d81aee80..63453ac34bc650e2e6693ce666d2465aea588c94 100644
--- a/app/src/main/java/se/leap/bitmaskclient/BaseConfigurationWizard.java
+++ b/app/src/main/java/se/leap/bitmaskclient/BaseConfigurationWizard.java
@@ -56,6 +56,7 @@ import se.leap.bitmaskclient.userstatus.SessionDialog;
 import static android.view.View.GONE;
 import static android.view.View.INVISIBLE;
 import static android.view.View.VISIBLE;
+import static se.leap.bitmaskclient.ProviderAPI.ERRORS;
 
 /**
  * abstract base Activity that builds and shows the list of known available providers.
@@ -184,10 +185,10 @@ public abstract class BaseConfigurationWizard extends Activity
         // by the height of mProgressbar (and the height of the first list item)
         mProgressBar.setVisibility(INVISIBLE);
         progressbar_description.setVisibility(INVISIBLE);
-
+        mProgressBar.setProgress(0);
     }
 
-    private void showProgressBar() {
+    protected void showProgressBar() {
         mProgressBar.setVisibility(VISIBLE);
         progressbar_description.setVisibility(VISIBLE);
     }
@@ -215,6 +216,8 @@ public abstract class BaseConfigurationWizard extends Activity
                 String provider_json_string = preferences.getString(Provider.KEY, "");
                 if (!provider_json_string.isEmpty())
                     selected_provider.define(new JSONObject(provider_json_string));
+                String caCert = preferences.getString(Provider.CA_CERT, "");
+                selected_provider.setCACert(caCert);
             } catch (JSONException e) {
                 e.printStackTrace();
             }
@@ -231,12 +234,11 @@ public abstract class BaseConfigurationWizard extends Activity
             }
         } else if (resultCode == ProviderAPI.PROVIDER_NOK) {
             mConfigState.setAction(PROVIDER_NOT_SET);
-            hideProgressBar();
             preferences.edit().remove(Provider.KEY).apply();
 
             setResult(RESULT_CANCELED, mConfigState);
 
-            String reason_to_fail = resultData.getString(ProviderAPI.ERRORS);
+            String reason_to_fail = resultData.getString(ERRORS);
             showDownloadFailedDialog(reason_to_fail);
         } else if (resultCode == ProviderAPI.CORRECTLY_DOWNLOADED_CERTIFICATE) {
             mProgressBar.incrementProgressBy(1);
@@ -293,12 +295,28 @@ public abstract class BaseConfigurationWizard extends Activity
         cancelSettingUpProvider();
     }
 
+    @Override
     public void cancelSettingUpProvider() {
+        hideProgressBar();
         mConfigState.setAction(PROVIDER_NOT_SET);
         adapter.showAllProviders();
         preferences.edit().remove(Provider.KEY).remove(Constants.PROVIDER_ALLOW_ANONYMOUS).remove(Constants.PROVIDER_KEY).apply();
     }
 
+    @Override
+    public void updateProviderDetails() {
+        mConfigState.setAction(SETTING_UP_PROVIDER);
+        Intent provider_API_command = new Intent(this, ProviderAPI.class);
+
+        provider_API_command.setAction(ProviderAPI.UPDATE_PROVIDER_DETAILS);
+        provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, providerAPI_result_receiver);
+        Bundle parameters = new Bundle();
+        parameters.putString(Provider.MAIN_URL, selected_provider.getMainUrl().toString());
+        provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters);
+
+        startService(provider_API_command);
+    }
+
     private void askDashboardToQuitApp() {
         Intent ask_quit = new Intent();
         ask_quit.putExtra(Dashboard.ACTION_QUIT, Dashboard.ACTION_QUIT);
@@ -334,7 +352,7 @@ public abstract class BaseConfigurationWizard extends Activity
     }
 
     /**
-     * Asks ProviderAPI to download an anonymous (anon) VPN certificate.
+     * Asks ProviderApiService to download an anonymous (anon) VPN certificate.
      */
     private void downloadVpnCertificate() {
         Intent provider_API_command = new Intent(this, ProviderAPI.class);
@@ -369,24 +387,28 @@ public abstract class BaseConfigurationWizard extends Activity
     /**
      * Shows an error dialog, if configuring of a provider failed.
      *
-     * @param reason_to_fail
+     * @param reasonToFail
      */
-    public void showDownloadFailedDialog(String reason_to_fail) {
+    public void showDownloadFailedDialog(String reasonToFail) {
         try {
             FragmentTransaction fragment_transaction = fragment_manager.removePreviousFragment(DownloadFailedDialog.TAG);
-
-            DialogFragment newFragment = DownloadFailedDialog.newInstance(reason_to_fail);
+            DialogFragment newFragment;
+            try {
+                JSONObject errorJson = new JSONObject(reasonToFail);
+                newFragment = DownloadFailedDialog.newInstance(errorJson);
+            } catch (JSONException e) {
+                e.printStackTrace();
+                newFragment = DownloadFailedDialog.newInstance(reasonToFail);
+            }
             newFragment.show(fragment_transaction, DownloadFailedDialog.TAG);
         } catch (IllegalStateException e) {
             e.printStackTrace();
             mConfigState.setAction(PENDING_SHOW_FAILED_DIALOG);
-            mConfigState.putExtra(REASON_TO_FAIL, reason_to_fail);
+            mConfigState.putExtra(REASON_TO_FAIL, reasonToFail);
         }
 
     }
 
-
-
     /**
      * Once selected a provider, this fragment offers the user to log in,
      * use it anonymously (if possible)
@@ -406,7 +428,6 @@ public abstract class BaseConfigurationWizard extends Activity
         }
     }
 
-
     @Override
     public boolean onCreateOptionsMenu(Menu menu) {
         getMenuInflater().inflate(R.menu.configuration_wizard_activity, menu);
diff --git a/app/src/main/java/se/leap/bitmaskclient/ConfigHelper.java b/app/src/main/java/se/leap/bitmaskclient/ConfigHelper.java
index fd1e2080ecf85588befddb52ed4b6280eaa26b78..0e8610591f903ee9aefb5bbe9c2b65a55b838d11 100644
--- a/app/src/main/java/se/leap/bitmaskclient/ConfigHelper.java
+++ b/app/src/main/java/se/leap/bitmaskclient/ConfigHelper.java
@@ -16,24 +16,44 @@
  */
 package se.leap.bitmaskclient;
 
-import android.util.*;
+import android.support.annotation.NonNull;
+import android.util.Log;
 
-import org.json.*;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.spongycastle.util.encoders.Base64;
 
-import java.io.*;
-import java.math.*;
-import java.security.*;
-import java.security.cert.*;
-import java.security.interfaces.*;
-import java.security.spec.*;
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
+import java.math.BigInteger;
+import java.security.KeyFactory;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.RSAPrivateKey;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+
+import static android.R.attr.name;
 
 /**
- * Stores constants, and implements auxiliary methods used across all LEAP Android classes.
+ * Stores constants, and implements auxiliary methods used across all Bitmask Android classes.
  *
  * @author parmegv
  * @author MeanderingCode
  */
 public class ConfigHelper {
+    private static final String TAG = ConfigHelper.class.getName();
     private static KeyStore keystore_trusted;
 
     final public static String NG_1024 =
@@ -42,7 +62,7 @@ public class ConfigHelper {
 
     public static boolean checkErroneousDownload(String downloaded_string) {
         try {
-            if (new JSONObject(downloaded_string).has(ProviderAPI.ERRORS) || downloaded_string.isEmpty()) {
+            if (downloaded_string == null || downloaded_string.isEmpty() || new JSONObject(downloaded_string).has(ProviderAPI.ERRORS)) {
                 return true;
             } else {
                 return false;
@@ -79,7 +99,7 @@ public class ConfigHelper {
             cf = CertificateFactory.getInstance("X.509");
 
             certificate_string = certificate_string.replaceFirst("-----BEGIN CERTIFICATE-----", "").replaceFirst("-----END CERTIFICATE-----", "").trim();
-            byte[] cert_bytes = Base64.decode(certificate_string, Base64.DEFAULT);
+            byte[] cert_bytes = Base64.decode(certificate_string);
             InputStream caInput = new ByteArrayInputStream(cert_bytes);
             try {
                 certificate = cf.generateCertificate(caInput);
@@ -87,24 +107,50 @@ public class ConfigHelper {
             } finally {
                 caInput.close();
             }
-        } catch (CertificateException e) {
-            // TODO Auto-generated catch block
-            e.printStackTrace();
-        } catch (IOException e) {
-            return null;
-        } catch (IllegalArgumentException e) {
+        } catch (NullPointerException | CertificateException | IOException | IllegalArgumentException e) {
             return null;
         }
-
         return (X509Certificate) certificate;
     }
 
+
+    public static String loadInputStreamAsString(InputStream inputStream) {
+        BufferedReader in = null;
+        try {
+            StringBuilder buf = new StringBuilder();
+            in = new BufferedReader(new InputStreamReader(inputStream));
+
+            String str;
+            boolean isFirst = true;
+            while ( (str = in.readLine()) != null ) {
+                if (isFirst)
+                    isFirst = false;
+                else
+                    buf.append('\n');
+                buf.append(str);
+            }
+            return buf.toString();
+        } catch (IOException e) {
+            Log.e(TAG, "Error opening asset " + name);
+        } finally {
+            if (in != null) {
+                try {
+                    in.close();
+                } catch (IOException e) {
+                    Log.e(TAG, "Error closing asset " + name);
+                }
+            }
+        }
+
+        return null;
+    }
+
     protected static RSAPrivateKey parseRsaKeyFromString(String rsaKeyString) {
         RSAPrivateKey key = null;
         try {
             KeyFactory kf = KeyFactory.getInstance("RSA", "BC");
             rsaKeyString = rsaKeyString.replaceFirst("-----BEGIN RSA PRIVATE KEY-----", "").replaceFirst("-----END RSA PRIVATE KEY-----", "");
-            PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Base64.decode(rsaKeyString, Base64.DEFAULT));
+            PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Base64.decode(rsaKeyString));
             key = (RSAPrivateKey) kf.generatePrivate(keySpec);
         } catch (InvalidKeySpecException e) {
             // TODO Auto-generated catch block
@@ -126,6 +172,32 @@ public class ConfigHelper {
         return key;
     }
 
+    private static String byteArrayToHex(byte[] input) {
+        int readBytes = input.length;
+        StringBuffer hexData = new StringBuffer();
+        int onebyte;
+        for (int i = 0; i < readBytes; i++) {
+            onebyte = ((0x000000ff & input[i]) | 0xffffff00);
+            hexData.append(Integer.toHexString(onebyte).substring(6));
+        }
+        return hexData.toString();
+    }
+
+    /**
+     * Calculates the hexadecimal representation of a sha256/sha1 fingerprint of a certificate
+     *
+     * @param certificate
+     * @param encoding
+     * @return
+     * @throws NoSuchAlgorithmException
+     * @throws CertificateEncodingException
+     */
+    @NonNull
+    public static String getFingerprintFromCertificate(X509Certificate certificate, String encoding) throws NoSuchAlgorithmException, CertificateEncodingException /*, UnsupportedEncodingException*/ {
+        byte[] byteArray = MessageDigest.getInstance(encoding).digest(certificate.getEncoded());
+        return byteArrayToHex(byteArray);
+    }
+    
     /**
      * Adds a new X509 certificate given its input stream and its provider name
      *
diff --git a/app/src/main/java/se/leap/bitmaskclient/Dashboard.java b/app/src/main/java/se/leap/bitmaskclient/Dashboard.java
index f1e7b3bdee957b281f9b2cc82dfa77334a2c25c5..861ce801ffa0ba3eeae6f9f2efb14bda8f401ae7 100644
--- a/app/src/main/java/se/leap/bitmaskclient/Dashboard.java
+++ b/app/src/main/java/se/leap/bitmaskclient/Dashboard.java
@@ -38,9 +38,13 @@ import org.json.JSONObject;
 
 import java.net.MalformedURLException;
 import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
 
 import butterknife.ButterKnife;
 import butterknife.InjectView;
+import de.blinkt.openvpn.core.VpnStatus;
 import se.leap.bitmaskclient.userstatus.SessionDialog;
 import se.leap.bitmaskclient.userstatus.User;
 import se.leap.bitmaskclient.userstatus.UserStatusFragment;
@@ -104,6 +108,11 @@ public class Dashboard extends Activity implements ProviderAPIResultReceiver.Rec
             handleVersion();
         }
 
+        // initialize app necessities
+        ProviderAPICommand.initialize(this);
+        VpnStatus.initLogCache(getApplicationContext().getCacheDir());
+        User.init(getString(R.string.default_username));
+
         prepareEIP(savedInstanceState);
     }
 
@@ -147,6 +156,7 @@ public class Dashboard extends Activity implements ProviderAPIResultReceiver.Rec
         try {
             provider.setUrl(new URL(preferences.getString(Provider.MAIN_URL, "")));
             provider.define(new JSONObject(preferences.getString(Provider.KEY, "")));
+            provider.setCACert(preferences.getString(Provider.CA_CERT, ""));
         } catch (MalformedURLException | JSONException e) {
             e.printStackTrace();
         }
@@ -246,10 +256,9 @@ public class Dashboard extends Activity implements ProviderAPIResultReceiver.Rec
     }
     @SuppressLint("CommitPrefEdits")
     private void providerToPreferences(Provider provider) {
-        //FIXME: figure out why .commit() is used and try refactor that cause, currently runs on UI thread
-        preferences.edit().putBoolean(Constants.PROVIDER_CONFIGURED, true).commit();
-        preferences.edit().putString(Provider.MAIN_URL, provider.mainUrl().toString()).apply();
-        preferences.edit().putString(Provider.KEY, provider.definition().toString()).apply();
+        preferences.edit().putBoolean(Constants.PROVIDER_CONFIGURED, true).
+                putString(Provider.MAIN_URL, provider.getMainUrl().toString()).
+                putString(Provider.KEY, provider.getDefinition().toString()).apply();
     }
 
     private void configErrorDialog() {
@@ -399,7 +408,25 @@ public class Dashboard extends Activity implements ProviderAPIResultReceiver.Rec
     private void switchProvider() {
         if (provider.hasEIP()) eip_fragment.stopEipIfPossible();
 
-        preferences.edit().clear().apply();
+        Map<String, ?> allEntries = preferences.getAll();
+        List<String> lastProvidersKeys = new ArrayList<>();
+        for (Map.Entry<String, ?> entry : allEntries.entrySet()) {
+            //sort out all preferences that don't belong to the last provider
+            if (entry.getKey().startsWith(Provider.KEY + ".") ||
+                    entry.getKey().startsWith(Provider.CA_CERT + ".") ||
+                    entry.getKey().startsWith(Provider.CA_CERT_FINGERPRINT + ".")
+                    ) {
+                continue;
+            }
+            lastProvidersKeys.add(entry.getKey());
+        }
+
+        SharedPreferences.Editor preferenceEditor = preferences.edit();
+        for (String key : lastProvidersKeys) {
+            preferenceEditor.remove(key);
+        }
+        preferenceEditor.apply();
+
         switching_provider = false;
         startActivityForResult(new Intent(this, ConfigurationWizard.class), SWITCH_PROVIDER);
     }
diff --git a/app/src/main/java/se/leap/bitmaskclient/DefaultedURL.java b/app/src/main/java/se/leap/bitmaskclient/DefaultedURL.java
index 8daa7d8c27081bc7871225b7f01758ffcd2e8882..52c797a4d22c6c87a6e84b6c932e227ea96cad59 100644
--- a/app/src/main/java/se/leap/bitmaskclient/DefaultedURL.java
+++ b/app/src/main/java/se/leap/bitmaskclient/DefaultedURL.java
@@ -31,6 +31,7 @@ public class DefaultedURL {
         return url;
     }
 
+    @Override
     public String toString() {
         return url.toString();
     }
diff --git a/app/src/main/java/se/leap/bitmaskclient/DownloadFailedDialog.java b/app/src/main/java/se/leap/bitmaskclient/DownloadFailedDialog.java
index da32dbd42a32927791433c36ef2b5fe40db91872..9d3f4b52e46e694e375f8c0ec23cb0d4e497af0a 100644
--- a/app/src/main/java/se/leap/bitmaskclient/DownloadFailedDialog.java
+++ b/app/src/main/java/se/leap/bitmaskclient/DownloadFailedDialog.java
@@ -16,9 +16,19 @@
  */
 package se.leap.bitmaskclient;
 
-import android.app.*;
-import android.content.*;
-import android.os.*;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.DialogInterface;
+import android.os.Bundle;
+
+import org.json.JSONObject;
+
+import static se.leap.bitmaskclient.DownloadFailedDialog.DOWNLOAD_ERRORS.DEFAULT;
+import static se.leap.bitmaskclient.DownloadFailedDialog.DOWNLOAD_ERRORS.valueOf;
+import static se.leap.bitmaskclient.ProviderAPI.ERRORID;
+import static se.leap.bitmaskclient.ProviderAPI.ERRORS;
 
 /**
  * Implements a dialog to show why a download failed.
@@ -29,6 +39,13 @@ public class DownloadFailedDialog extends DialogFragment {
 
     public static String TAG = "downloaded_failed_dialog";
     private String reason_to_fail;
+    private DOWNLOAD_ERRORS downloadError = DEFAULT;
+    public enum DOWNLOAD_ERRORS {
+        DEFAULT,
+        ERROR_CORRUPTED_PROVIDER_JSON,
+        ERROR_INVALID_CERTIFICATE,
+        ERROR_CERTIFICATE_PINNING
+    }
 
     /**
      * @return a new instance of this DialogFragment.
@@ -39,32 +56,79 @@ public class DownloadFailedDialog extends DialogFragment {
         return dialog_fragment;
     }
 
+    /**
+     * @return a new instance of this DialogFragment.
+     */
+    public static DialogFragment newInstance(JSONObject errorJson) {
+        DownloadFailedDialog dialog_fragment = new DownloadFailedDialog();
+        try {
+            if (errorJson.has(ERRORS)) {
+                dialog_fragment.reason_to_fail = errorJson.getString(ERRORS);
+            } else {
+                //default error msg
+                dialog_fragment.reason_to_fail = dialog_fragment.getString(R.string.error_io_exception_user_message);
+            }
+
+            if (errorJson.has(ERRORID)) {
+                dialog_fragment.downloadError = valueOf(errorJson.getString(ERRORID));
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            dialog_fragment.reason_to_fail = dialog_fragment.getString(R.string.error_io_exception_user_message);
+        }
+        return dialog_fragment;
+    }
+
     @Override
     public Dialog onCreateDialog(Bundle savedInstanceState) {
         AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
-
         builder.setMessage(reason_to_fail)
-                .setPositiveButton(R.string.retry, new DialogInterface.OnClickListener() {
-                    public void onClick(DialogInterface dialog, int id) {
+                .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
+            public void onClick(DialogInterface dialog, int id) {
+                interface_with_ConfigurationWizard.cancelSettingUpProvider();
+                dialog.dismiss();
+            }
+        });
+        switch (downloadError) {
+            case ERROR_CORRUPTED_PROVIDER_JSON:
+                builder.setPositiveButton(R.string.update_provider_details, new DialogInterface.OnClickListener() {
+                    @Override
+                    public void onClick(DialogInterface dialog, int which) {
                         dismiss();
-                        interface_with_ConfigurationWizard.retrySetUpProvider();
+                        interface_with_ConfigurationWizard.updateProviderDetails();
                     }
-                })
-                .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
-                    public void onClick(DialogInterface dialog, int id) {
-                        interface_with_ConfigurationWizard.cancelSettingUpProvider();
-                        dialog.dismiss();
+                });
+                break;
+            case ERROR_CERTIFICATE_PINNING:
+            case ERROR_INVALID_CERTIFICATE:
+                builder.setPositiveButton(R.string.update_certificate, new DialogInterface.OnClickListener() {
+                    @Override
+                    public void onClick(DialogInterface dialog, int which) {
+                        dismiss();
+                        interface_with_ConfigurationWizard.updateProviderDetails();
                     }
                 });
+                break;
+            default:
+                builder.setPositiveButton(R.string.retry, new DialogInterface.OnClickListener() {
+                            public void onClick(DialogInterface dialog, int id) {
+                                dismiss();
+                                interface_with_ConfigurationWizard.retrySetUpProvider();
+                            }
+                        });
+                break;
+        }
 
         // Create the AlertDialog object and return it
         return builder.create();
     }
 
     public interface DownloadFailedDialogInterface {
-        public void retrySetUpProvider();
+        void retrySetUpProvider();
+
+        void cancelSettingUpProvider();
 
-        public void cancelSettingUpProvider();
+        void updateProviderDetails();
     }
 
     DownloadFailedDialogInterface interface_with_ConfigurationWizard;
diff --git a/app/src/main/java/se/leap/bitmaskclient/OkHttpClientGenerator.java b/app/src/main/java/se/leap/bitmaskclient/OkHttpClientGenerator.java
new file mode 100644
index 0000000000000000000000000000000000000000..1bf679f8a5d5ec0d77a5f7e4aece3d0f64c56300
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/OkHttpClientGenerator.java
@@ -0,0 +1,166 @@
+/**
+ * Copyright (c) 2018 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package se.leap.bitmaskclient;
+
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.os.Build;
+import android.support.annotation.NonNull;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.net.UnknownHostException;
+import java.security.KeyManagementException;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.cert.CertificateException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+
+import okhttp3.CipherSuite;
+import okhttp3.ConnectionSpec;
+import okhttp3.Cookie;
+import okhttp3.CookieJar;
+import okhttp3.HttpUrl;
+import okhttp3.OkHttpClient;
+import okhttp3.TlsVersion;
+
+import static android.text.TextUtils.isEmpty;
+import static se.leap.bitmaskclient.ProviderAPI.ERRORS;
+import static se.leap.bitmaskclient.R.string.certificate_error;
+import static se.leap.bitmaskclient.R.string.error_io_exception_user_message;
+import static se.leap.bitmaskclient.R.string.error_no_such_algorithm_exception_user_message;
+import static se.leap.bitmaskclient.R.string.keyChainAccessError;
+import static se.leap.bitmaskclient.R.string.server_unreachable_message;
+
+/**
+ * Created by cyberta on 08.01.18.
+ */
+
+public class OkHttpClientGenerator {
+
+    SharedPreferences preferences;
+    Resources resources;
+
+    public OkHttpClientGenerator(SharedPreferences preferences, Resources resources) {
+        this.preferences = preferences;
+        this.resources = resources;
+    }
+
+    public OkHttpClient initCommercialCAHttpClient(JSONObject initError) {
+        return initHttpClient(initError, null);
+    }
+
+    public OkHttpClient initSelfSignedCAHttpClient(JSONObject initError) {
+        String certificate = preferences.getString(Provider.CA_CERT, "");
+        return initHttpClient(initError, certificate);
+    }
+
+    public OkHttpClient initSelfSignedCAHttpClient(JSONObject initError, String certificate) {
+        return initHttpClient(initError, certificate);
+    }
+
+
+    private OkHttpClient initHttpClient(JSONObject initError, String certificate) {
+        try {
+            TLSCompatSocketFactory sslCompatFactory;
+            ConnectionSpec spec = getConnectionSpec();
+            OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder();
+
+            if (!isEmpty(certificate)) {
+                sslCompatFactory = new TLSCompatSocketFactory(certificate);
+            } else {
+                sslCompatFactory = new TLSCompatSocketFactory();
+            }
+            sslCompatFactory.initSSLSocketFactory(clientBuilder);
+            clientBuilder.cookieJar(getCookieJar())
+                    .connectionSpecs(Collections.singletonList(spec));
+            return clientBuilder.build();
+        } catch (IllegalArgumentException e) {
+            e.printStackTrace();
+            addErrorMessageToJson(initError, resources.getString(R.string.certificate_error));
+        } catch (IllegalStateException | KeyManagementException | KeyStoreException e) {
+            e.printStackTrace();
+            addErrorMessageToJson(initError, String.format(resources.getString(keyChainAccessError), e.getLocalizedMessage()));
+        } catch (NoSuchAlgorithmException | NoSuchProviderException e) {
+            e.printStackTrace();
+            addErrorMessageToJson(initError, resources.getString(error_no_such_algorithm_exception_user_message));
+        } catch (CertificateException e) {
+            e.printStackTrace();
+            addErrorMessageToJson(initError, resources.getString(certificate_error));
+        } catch (UnknownHostException e) {
+            e.printStackTrace();
+            addErrorMessageToJson(initError, resources.getString(server_unreachable_message));
+        } catch (IOException e) {
+            e.printStackTrace();
+            addErrorMessageToJson(initError, resources.getString(error_io_exception_user_message));
+        }
+        return null;
+    }
+
+
+
+    @NonNull
+    private ConnectionSpec getConnectionSpec() {
+        ConnectionSpec.Builder connectionSpecbuilder = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
+                .tlsVersions(TlsVersion.TLS_1_2, TlsVersion.TLS_1_3);
+        //FIXME: restrict connection further to the following recommended cipher suites for ALL supported API levels
+        //figure out how to use bcjsse for that purpose
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1)
+            connectionSpecbuilder.cipherSuites(
+                    CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
+                    CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
+                    CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
+                    CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
+                    CipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
+                    CipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
+            );
+        return connectionSpecbuilder.build();
+    }
+
+    @NonNull
+    private CookieJar getCookieJar() {
+        return new CookieJar() {
+            private final HashMap<String, List<Cookie>> cookieStore = new HashMap<>();
+
+            @Override
+            public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
+                cookieStore.put(url.host(), cookies);
+            }
+
+            @Override
+            public List<Cookie> loadForRequest(HttpUrl url) {
+                List<Cookie> cookies = cookieStore.get(url.host());
+                return cookies != null ? cookies : new ArrayList<Cookie>();
+            }
+        };
+    }
+
+    private void addErrorMessageToJson(JSONObject jsonObject, String errorMessage) {
+        try {
+            jsonObject.put(ERRORS, errorMessage);
+        } catch (JSONException e) {
+            e.printStackTrace();
+        }
+    }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/Provider.java b/app/src/main/java/se/leap/bitmaskclient/Provider.java
index 559b47d1851dfd8554be5b93072fad3eb3efffc0..60b1b93c4e57ed71d8834812a96d1a08bc930eef 100644
--- a/app/src/main/java/se/leap/bitmaskclient/Provider.java
+++ b/app/src/main/java/se/leap/bitmaskclient/Provider.java
@@ -18,9 +18,11 @@ package se.leap.bitmaskclient;
 
 import android.os.*;
 
+import com.google.gson.Gson;
+
 import org.json.*;
 
-import java.io.*;
+import java.io.Serializable;
 import java.net.*;
 import java.util.*;
 
@@ -31,8 +33,11 @@ import java.util.*;
 public final class Provider implements Parcelable {
 
     private JSONObject definition = new JSONObject(); // Represents our Provider's provider.json
-    private DefaultedURL main_url = new DefaultedURL();
-    private String certificate_pin = "";
+    private DefaultedURL mainUrl = new DefaultedURL();
+    private DefaultedURL apiUrl = new DefaultedURL();
+    private String certificatePin = "";
+    private String certificatePinEncoding = "";
+    private String caCert = "";
 
     final public static String
             API_URL = "api_uri",
@@ -61,13 +66,24 @@ public final class Provider implements Parcelable {
 
     public Provider() { }
 
-    public Provider(URL main_url) {
-        this.main_url.setUrl(main_url);
+    public Provider(URL mainUrl) {
+        this.mainUrl.setUrl(mainUrl);
     }
 
-    public Provider(URL main_url, String certificate_pin) {
-        this.main_url.setUrl(main_url);
-        this.certificate_pin = certificate_pin;
+    public Provider(URL mainUrl, String caCert, String definition) {
+        this.mainUrl.setUrl(mainUrl);
+        if (caCert != null) {
+            this.caCert = caCert;
+        }
+        if (definition != null) {
+            try {
+                this.definition = new JSONObject(definition);
+                parseDefinition(this.definition);
+            } catch (JSONException | NullPointerException e) {
+                e.printStackTrace();
+            }
+        }
+
     }
 
     public static final Parcelable.Creator<Provider> CREATOR
@@ -81,42 +97,57 @@ public final class Provider implements Parcelable {
         }
     };
 
-    private Provider(Parcel in) {
-        try {
-            main_url.setUrl(new URL(in.readString()));
-            String definition_string = in.readString();
-            if (!definition_string.isEmpty())
-                definition = new JSONObject((definition_string));
-        } catch (MalformedURLException | JSONException e) {
-            e.printStackTrace();
-        }
-    }
-
     public boolean isConfigured() {
-        return !main_url.isDefault() && definition.length() > 0;
+        return !mainUrl.isDefault() &&
+                definition.length() > 0 &&
+                !apiUrl.isDefault() &&
+                caCert != null &&
+                !caCert.isEmpty();
     }
 
     protected void setUrl(URL url) {
-        main_url.setUrl(url);
+        mainUrl.setUrl(url);
     }
 
     protected void define(JSONObject provider_json) {
         definition = provider_json;
+        parseDefinition(definition);
     }
 
-    protected JSONObject definition() {
+    protected JSONObject getDefinition() {
         return definition;
     }
 
     protected String getDomain() {
-        return main_url.getDomain();
+        return mainUrl.getDomain();
+    }
+
+    protected DefaultedURL getMainUrl() {
+        return mainUrl;
+    }
+
+    protected DefaultedURL getApiUrl() {
+        return apiUrl;
+    }
+
+    protected String certificatePin() { return certificatePin; }
+
+    protected boolean hasCertificatePin() {
+        return certificatePin != null && !certificatePin.isEmpty();
+    }
+
+    boolean hasCaCert() {
+        return caCert != null && !caCert.isEmpty();
     }
 
-    protected DefaultedURL mainUrl() {
-        return main_url;
+    public boolean hasDefinition() {
+        return definition != null && definition.length() > 0;
     }
 
-    protected String certificatePin() { return certificate_pin; }
+
+    public String getCaCert() {
+        return caCert;
+    }
 
     public String getName() {
         // Should we pass the locale in, or query the system here?
@@ -127,8 +158,8 @@ public final class Provider implements Parcelable {
                 name = definition.getJSONObject(API_TERM_NAME).getString(lang);
             else throw new JSONException("Provider not defined");
         } catch (JSONException e) {
-            if (main_url != null) {
-                String host = main_url.getDomain();
+            if (mainUrl != null) {
+                String host = mainUrl.getDomain();
                 name = host.substring(0, host.indexOf("."));
             }
         }
@@ -191,24 +222,29 @@ public final class Provider implements Parcelable {
 
     @Override
     public void writeToParcel(Parcel parcel, int i) {
-        if(main_url != null)
-            parcel.writeString(main_url.toString());
+        if(mainUrl != null)
+            parcel.writeString(mainUrl.toString());
         if (definition != null)
             parcel.writeString(definition.toString());
+        if (caCert != null)
+            parcel.writeString(caCert);
     }
 
     @Override
     public boolean equals(Object o) {
         if (o instanceof Provider) {
             Provider p = (Provider) o;
-            return p.mainUrl().getDomain().equals(mainUrl().getDomain());
+            return p.getDomain().equals(getDomain());
         } else return false;
     }
 
     public JSONObject toJson() {
         JSONObject json = new JSONObject();
         try {
-            json.put(Provider.MAIN_URL, main_url);
+            json.put(Provider.MAIN_URL, mainUrl);
+            //TODO: add other fields here?
+            //this is used to save custom providers as json. I guess this doesn't work correctly
+            //TODO 2: verify that
         } catch (JSONException e) {
             e.printStackTrace();
         }
@@ -217,6 +253,45 @@ public final class Provider implements Parcelable {
 
     @Override
     public int hashCode() {
-        return mainUrl().getDomain().hashCode();
+        return getDomain().hashCode();
+    }
+
+    @Override
+    public String toString() {
+        return new Gson().toJson(this);
+    }
+
+    //TODO: write a test for marshalling!
+    private Provider(Parcel in) {
+        try {
+            mainUrl.setUrl(new URL(in.readString()));
+            String definitionString = in.readString();
+            if (!definitionString.isEmpty()) {
+                definition = new JSONObject((definitionString));
+                parseDefinition(definition);
+            }
+            String caCert = in.readString();
+            if (!caCert.isEmpty()) {
+                this.caCert = caCert;
+            }
+        } catch (MalformedURLException | JSONException e) {
+            e.printStackTrace();
+        }
     }
+
+    private void parseDefinition(JSONObject definition) {
+        try {
+            String pin =  definition.getString(CA_CERT_FINGERPRINT);
+            this.certificatePin = pin.split(":")[1].trim();
+            this.certificatePinEncoding = pin.split(":")[0].trim();
+            this.apiUrl.setUrl(new URL(definition.getString(API_URL)));
+        } catch (JSONException | ArrayIndexOutOfBoundsException | MalformedURLException e) {
+            e.printStackTrace();
+        }
+    }
+
+    public void setCACert(String cert) {
+        this.caCert = cert;
+    }
+
 }
diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderAPI.java b/app/src/main/java/se/leap/bitmaskclient/ProviderAPI.java
new file mode 100644
index 0000000000000000000000000000000000000000..5a6aabc0279c0ff52071f87252da687dfacb36a4
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/ProviderAPI.java
@@ -0,0 +1,124 @@
+/**
+ * Copyright (c) 2017 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package se.leap.bitmaskclient;
+
+import android.annotation.SuppressLint;
+import android.app.IntentService;
+import android.content.Intent;
+import android.content.SharedPreferences;
+
+import de.blinkt.openvpn.core.Preferences;
+
+/**
+ * Implements HTTP api methods (encapsulated in {{@link ProviderApiManager}})
+ * used to manage communications with the provider server.
+ * <p/>
+ * It's an IntentService because it downloads data from the Internet, so it operates in the background.
+ *
+ * @author parmegv
+ * @author MeanderingCode
+ * @author cyberta
+ */
+
+public class ProviderAPI extends IntentService implements ProviderApiManagerBase.ProviderApiServiceCallback {
+
+    final public static String
+            TAG = ProviderAPI.class.getSimpleName(),
+            SET_UP_PROVIDER = "setUpProvider",
+            UPDATE_PROVIDER_DETAILS = "updateProviderDetails",
+            DOWNLOAD_NEW_PROVIDER_DOTJSON = "downloadNewProviderDotJSON",
+            SIGN_UP = "srpRegister",
+            LOG_IN = "srpAuth",
+            LOG_OUT = "logOut",
+            DOWNLOAD_CERTIFICATE = "downloadUserAuthedCertificate",
+            PARAMETERS = "parameters",
+            RESULT_KEY = "result",
+            RECEIVER_KEY = "receiver",
+            ERRORS = "errors",
+            ERRORID = "errorId",
+            UPDATE_PROGRESSBAR = "update_progressbar",
+            CURRENT_PROGRESS = "current_progress",
+            DOWNLOAD_EIP_SERVICE = TAG + ".DOWNLOAD_EIP_SERVICE";
+
+    final public static int
+            SUCCESSFUL_LOGIN = 3,
+            FAILED_LOGIN = 4,
+            SUCCESSFUL_SIGNUP = 5,
+            FAILED_SIGNUP = 6,
+            SUCCESSFUL_LOGOUT = 7,
+            LOGOUT_FAILED = 8,
+            CORRECTLY_DOWNLOADED_CERTIFICATE = 9,
+            INCORRECTLY_DOWNLOADED_CERTIFICATE = 10,
+            PROVIDER_OK = 11,
+            PROVIDER_NOK = 12,
+            CORRECTLY_DOWNLOADED_EIP_SERVICE = 13,
+            INCORRECTLY_DOWNLOADED_EIP_SERVICE = 14;
+
+    ProviderApiManager providerApiManager;
+
+
+
+    public ProviderAPI() {
+        super(TAG);
+    }
+
+    //TODO: refactor me, please!
+    public static void stop() {
+        ProviderApiManager.stop();
+    }
+
+    //TODO: refactor me, please!
+    public static boolean caCertDownloaded() {
+        return ProviderApiManager.caCertDownloaded();
+    }
+
+    //TODO: refactor me, please!
+    public static String lastProviderMainUrl() {
+        return ProviderApiManager.lastProviderMainUrl();
+    }
+
+    //TODO: refactor me, please!
+    //used in insecure flavor only
+    @SuppressLint("unused")
+    public static boolean lastDangerOn() {
+        return ProviderApiManager.lastDangerOn();
+    }
+
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        providerApiManager = initApiManager();
+    }
+
+    @Override
+    public void broadcastProgress(Intent intent) {
+        sendBroadcast(intent);
+    }
+
+    @Override
+    protected void onHandleIntent(Intent command) {
+        providerApiManager.handleIntent(command);
+    }
+
+
+    private ProviderApiManager initApiManager() {
+        SharedPreferences preferences = getSharedPreferences(Constants.SHARED_PREFERENCES, MODE_PRIVATE);
+        OkHttpClientGenerator clientGenerator = new OkHttpClientGenerator(preferences, getResources());
+        return new ProviderApiManager(preferences, getResources(), clientGenerator, this);
+    }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderAPIResultReceiver.java b/app/src/main/java/se/leap/bitmaskclient/ProviderAPIResultReceiver.java
index 9b880f89da38bf85263c0257a12e75ebc070fbd1..9b777e5ac7fa45f458f5925acf52c6486983594a 100644
--- a/app/src/main/java/se/leap/bitmaskclient/ProviderAPIResultReceiver.java
+++ b/app/src/main/java/se/leap/bitmaskclient/ProviderAPIResultReceiver.java
@@ -16,7 +16,9 @@
  */
 package se.leap.bitmaskclient;
 
-import android.os.*;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.ResultReceiver;
 
 /**
  * Implements the ResultReceiver needed by Activities using ProviderAPI to receive the results of its operations.
diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderApiConnector.java b/app/src/main/java/se/leap/bitmaskclient/ProviderApiConnector.java
new file mode 100644
index 0000000000000000000000000000000000000000..af79a95e57eabc05572c523e38505e694e7ad28a
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/ProviderApiConnector.java
@@ -0,0 +1,98 @@
+/**
+ * Copyright (c) 2018 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package se.leap.bitmaskclient;
+
+import android.support.annotation.NonNull;
+import android.util.Pair;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+import java.util.Locale;
+import java.util.Scanner;
+
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+
+/**
+ * Created by cyberta on 08.01.18.
+ */
+
+public class ProviderApiConnector {
+
+    private static final MediaType JSON
+            = MediaType.parse("application/json; charset=utf-8");
+
+
+    public static boolean delete(OkHttpClient okHttpClient, String deleteUrl) {
+        try {
+            Request.Builder requestBuilder = new Request.Builder()
+                    .url(deleteUrl)
+                    .delete();
+            Request request = requestBuilder.build();
+
+            Response response = okHttpClient.newCall(request).execute();
+            //response code 401: already logged out
+            if (response.isSuccessful() || response.code() == 401) {
+                return true;
+            }
+        }  catch (IOException | RuntimeException e) {
+            return false;
+        }
+
+        return false;
+    }
+
+    public static boolean canConnect(@NonNull OkHttpClient okHttpClient, String url) throws RuntimeException, IOException {
+        Request.Builder requestBuilder = new Request.Builder()
+                .url(url)
+                .method("GET", null);
+        Request request = requestBuilder.build();
+
+        Response response = okHttpClient.newCall(request).execute();
+        return response.isSuccessful();
+
+    }
+
+    public static String requestStringFromServer(@NonNull String url, @NonNull String request_method, String jsonString, @NonNull List<Pair<String, String>> headerArgs, @NonNull OkHttpClient okHttpClient) throws RuntimeException, IOException {
+
+        RequestBody jsonBody = jsonString != null ? RequestBody.create(JSON, jsonString) : null;
+        Request.Builder requestBuilder = new Request.Builder()
+                .url(url)
+                .method(request_method, jsonBody);
+        for (Pair<String, String> keyValPair : headerArgs) {
+            requestBuilder.addHeader(keyValPair.first, keyValPair.second);
+        }
+
+        //TODO: move to getHeaderArgs()?
+        String locale = Locale.getDefault().getLanguage() + Locale.getDefault().getCountry();
+        requestBuilder.addHeader("Accept-Language", locale);
+        Request request = requestBuilder.build();
+
+        Response response = okHttpClient.newCall(request).execute();
+        InputStream inputStream = response.body().byteStream();
+        Scanner scanner = new Scanner(inputStream).useDelimiter("\\A");
+        if (scanner.hasNext()) {
+            return scanner.next();
+        }
+        return null;
+    }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderApiBase.java b/app/src/main/java/se/leap/bitmaskclient/ProviderApiManagerBase.java
similarity index 58%
rename from app/src/main/java/se/leap/bitmaskclient/ProviderApiBase.java
rename to app/src/main/java/se/leap/bitmaskclient/ProviderApiManagerBase.java
index 6e3b8b08f96e700b42ddb47cb50fb3c3333d88b7..9f5fdc2d432c13ea20bdb8f7e267763a3d70ae66 100644
--- a/app/src/main/java/se/leap/bitmaskclient/ProviderApiBase.java
+++ b/app/src/main/java/se/leap/bitmaskclient/ProviderApiManagerBase.java
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2017 LEAP Encryption Access Project and contributers
+ * Copyright (c) 2018 LEAP Encryption Access Project and contributers
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -14,13 +14,12 @@
  * You should have received a copy of the GNU General Public License
  * along with this program. If not, see <http://www.gnu.org/licenses/>.
  */
+
 package se.leap.bitmaskclient;
 
-import android.app.IntentService;
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.content.res.Resources;
-import android.os.Build;
 import android.os.Bundle;
 import android.os.ResultReceiver;
 import android.support.annotation.NonNull;
@@ -31,150 +30,140 @@ import org.json.JSONException;
 import org.json.JSONObject;
 
 import java.io.IOException;
-import java.io.InputStream;
 import java.math.BigInteger;
 import java.net.ConnectException;
 import java.net.MalformedURLException;
 import java.net.SocketTimeoutException;
 import java.net.UnknownHostException;
 import java.net.UnknownServiceException;
-import java.security.KeyManagementException;
-import java.security.KeyStoreException;
-import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
-import java.security.NoSuchProviderException;
 import java.security.cert.CertificateEncodingException;
 import java.security.cert.CertificateException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
 import java.security.cert.X509Certificate;
 import java.security.interfaces.RSAPrivateKey;
 import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Locale;
-import java.util.Scanner;
 
 import javax.net.ssl.SSLHandshakeException;
 
-import okhttp3.CipherSuite;
-import okhttp3.ConnectionSpec;
-import okhttp3.Cookie;
-import okhttp3.CookieJar;
-import okhttp3.HttpUrl;
-import okhttp3.MediaType;
 import okhttp3.OkHttpClient;
-import okhttp3.Request;
-import okhttp3.RequestBody;
-import okhttp3.Response;
-import okhttp3.TlsVersion;
 import se.leap.bitmaskclient.userstatus.SessionDialog;
 import se.leap.bitmaskclient.userstatus.User;
 import se.leap.bitmaskclient.userstatus.UserStatus;
 
+import static se.leap.bitmaskclient.ConfigHelper.getFingerprintFromCertificate;
+import static se.leap.bitmaskclient.DownloadFailedDialog.DOWNLOAD_ERRORS.ERROR_CERTIFICATE_PINNING;
+import static se.leap.bitmaskclient.DownloadFailedDialog.DOWNLOAD_ERRORS.ERROR_CORRUPTED_PROVIDER_JSON;
+import static se.leap.bitmaskclient.DownloadFailedDialog.DOWNLOAD_ERRORS.ERROR_INVALID_CERTIFICATE;
+import static se.leap.bitmaskclient.Provider.MAIN_URL;
+import static se.leap.bitmaskclient.ProviderAPI.CORRECTLY_DOWNLOADED_CERTIFICATE;
+import static se.leap.bitmaskclient.ProviderAPI.CORRECTLY_DOWNLOADED_EIP_SERVICE;
+import static se.leap.bitmaskclient.ProviderAPI.CURRENT_PROGRESS;
+import static se.leap.bitmaskclient.ProviderAPI.DOWNLOAD_CERTIFICATE;
+import static se.leap.bitmaskclient.ProviderAPI.DOWNLOAD_EIP_SERVICE;
+import static se.leap.bitmaskclient.ProviderAPI.ERRORID;
+import static se.leap.bitmaskclient.ProviderAPI.ERRORS;
+import static se.leap.bitmaskclient.ProviderAPI.FAILED_LOGIN;
+import static se.leap.bitmaskclient.ProviderAPI.FAILED_SIGNUP;
+import static se.leap.bitmaskclient.ProviderAPI.INCORRECTLY_DOWNLOADED_CERTIFICATE;
+import static se.leap.bitmaskclient.ProviderAPI.INCORRECTLY_DOWNLOADED_EIP_SERVICE;
+import static se.leap.bitmaskclient.ProviderAPI.LOGOUT_FAILED;
+import static se.leap.bitmaskclient.ProviderAPI.LOG_IN;
+import static se.leap.bitmaskclient.ProviderAPI.LOG_OUT;
+import static se.leap.bitmaskclient.ProviderAPI.PARAMETERS;
+import static se.leap.bitmaskclient.ProviderAPI.PROVIDER_NOK;
+import static se.leap.bitmaskclient.ProviderAPI.PROVIDER_OK;
+import static se.leap.bitmaskclient.ProviderAPI.RECEIVER_KEY;
+import static se.leap.bitmaskclient.ProviderAPI.RESULT_KEY;
+import static se.leap.bitmaskclient.ProviderAPI.SET_UP_PROVIDER;
+import static se.leap.bitmaskclient.ProviderAPI.SIGN_UP;
+import static se.leap.bitmaskclient.ProviderAPI.SUCCESSFUL_LOGIN;
+import static se.leap.bitmaskclient.ProviderAPI.SUCCESSFUL_LOGOUT;
+import static se.leap.bitmaskclient.ProviderAPI.SUCCESSFUL_SIGNUP;
+import static se.leap.bitmaskclient.ProviderAPI.UPDATE_PROGRESSBAR;
+import static se.leap.bitmaskclient.ProviderAPI.UPDATE_PROVIDER_DETAILS;
 import static se.leap.bitmaskclient.R.string.certificate_error;
 import static se.leap.bitmaskclient.R.string.error_io_exception_user_message;
 import static se.leap.bitmaskclient.R.string.error_json_exception_user_message;
 import static se.leap.bitmaskclient.R.string.error_no_such_algorithm_exception_user_message;
-import static se.leap.bitmaskclient.R.string.keyChainAccessError;
 import static se.leap.bitmaskclient.R.string.malformed_url;
 import static se.leap.bitmaskclient.R.string.server_unreachable_message;
 import static se.leap.bitmaskclient.R.string.service_is_down_error;
+import static se.leap.bitmaskclient.R.string.warning_corrupted_provider_cert;
+import static se.leap.bitmaskclient.R.string.warning_corrupted_provider_details;
+import static se.leap.bitmaskclient.R.string.warning_expired_provider_cert;
 
 /**
- * Implements HTTP api methods used to manage communications with the provider server.
- * The implemented methods are commonly used by insecure's and production's flavor of ProviderAPI.
- * <p/>
- * It's an IntentService because it downloads data from the Internet, so it operates in the background.
- *
- * @author parmegv
- * @author MeanderingCode
- * @author cyberta
+ * Implements the logic of the http api calls. The methods of this class needs to be called from
+ * a background thread.
  */
 
-public abstract class ProviderApiBase extends IntentService {
-
-    final public static String
-            TAG = ProviderAPI.class.getSimpleName(),
-            SET_UP_PROVIDER = "setUpProvider",
-            DOWNLOAD_NEW_PROVIDER_DOTJSON = "downloadNewProviderDotJSON",
-            SIGN_UP = "srpRegister",
-            LOG_IN = "srpAuth",
-            LOG_OUT = "logOut",
-            DOWNLOAD_CERTIFICATE = "downloadUserAuthedCertificate",
-            PARAMETERS = "parameters",
-            RESULT_KEY = "result",
-            RECEIVER_KEY = "receiver",
-            ERRORS = "errors",
-            UPDATE_PROGRESSBAR = "update_progressbar",
-            CURRENT_PROGRESS = "current_progress",
-            DOWNLOAD_EIP_SERVICE = TAG + ".DOWNLOAD_EIP_SERVICE";
-
-    final public static int
-            SUCCESSFUL_LOGIN = 3,
-            FAILED_LOGIN = 4,
-            SUCCESSFUL_SIGNUP = 5,
-            FAILED_SIGNUP = 6,
-            SUCCESSFUL_LOGOUT = 7,
-            LOGOUT_FAILED = 8,
-            CORRECTLY_DOWNLOADED_CERTIFICATE = 9,
-            INCORRECTLY_DOWNLOADED_CERTIFICATE = 10,
-            PROVIDER_OK = 11,
-            PROVIDER_NOK = 12,
-            CORRECTLY_DOWNLOADED_EIP_SERVICE = 13,
-            INCORRECTLY_DOWNLOADED_EIP_SERVICE = 14;
-
-    public static boolean
+public abstract class ProviderApiManagerBase {
+
+    public interface ProviderApiServiceCallback {
+        void broadcastProgress(Intent intent);
+    }
+
+    private ProviderApiServiceCallback serviceCallback;
+
+    protected static volatile boolean
             CA_CERT_DOWNLOADED = false,
             PROVIDER_JSON_DOWNLOADED = false,
             EIP_SERVICE_JSON_DOWNLOADED = false;
 
-    protected static String last_provider_main_url;
+    protected static String lastProviderMainUrl;
     protected static boolean go_ahead = true;
     protected static SharedPreferences preferences;
-    protected static String provider_api_url;
-    protected static String provider_ca_cert_fingerprint;
+    protected static String providerApiUrl;
+    protected static String providerCaCertFingerprint;
+    protected static String providerCaCert;
+    protected static JSONObject providerDefinition;
     protected Resources resources;
+    protected OkHttpClientGenerator clientGenerator;
 
     public static void stop() {
         go_ahead = false;
     }
 
-    private final MediaType JSON
-            = MediaType.parse("application/json; charset=utf-8");
-
-    public ProviderApiBase() {
-        super(TAG);
-    }
-
-    @Override
-    public void onCreate() {
-        super.onCreate();
-
-        preferences = getSharedPreferences(Constants.SHARED_PREFERENCES, MODE_PRIVATE);
-        resources = getResources();
+    public static String lastProviderMainUrl() {
+        return lastProviderMainUrl;
     }
 
-    public static String lastProviderMainUrl() {
-        return last_provider_main_url;
+    public ProviderApiManagerBase(SharedPreferences preferences, Resources resources, OkHttpClientGenerator clientGenerator, ProviderApiServiceCallback callback) {
+        this.preferences = preferences;
+        this.resources = resources;
+        this.serviceCallback = callback;
+        this.clientGenerator = clientGenerator;
     }
 
-    @Override
-    protected void onHandleIntent(Intent command) {
+    public void handleIntent(Intent command) {
         final ResultReceiver receiver = command.getParcelableExtra(RECEIVER_KEY);
         String action = command.getAction();
         Bundle parameters = command.getBundleExtra(PARAMETERS);
 
-        if (provider_api_url == null && preferences.contains(Provider.KEY)) {
+        if (providerApiUrl == null && preferences.contains(Provider.KEY)) {
             try {
                 JSONObject provider_json = new JSONObject(preferences.getString(Provider.KEY, ""));
-                provider_api_url = provider_json.getString(Provider.API_URL) + "/" + provider_json.getString(Provider.API_VERSION);
+                providerApiUrl = provider_json.getString(Provider.API_URL) + "/" + provider_json.getString(Provider.API_VERSION);
                 go_ahead = true;
             } catch (JSONException e) {
                 go_ahead = false;
             }
         }
 
-        if (action.equalsIgnoreCase(SET_UP_PROVIDER)) {
+        if (action.equals(UPDATE_PROVIDER_DETAILS)) {
+            resetProviderDetails();
+            Bundle task = new Bundle();
+            task.putString(MAIN_URL, lastProviderMainUrl);
+            Bundle result = setUpProvider(task);
+            if (result.getBoolean(RESULT_KEY)) {
+                receiver.send(PROVIDER_OK, result);
+            } else {
+                receiver.send(PROVIDER_NOK, result);
+            }
+        } else if (action.equalsIgnoreCase(SET_UP_PROVIDER)) {
             Bundle result = setUpProvider(parameters);
             if (go_ahead) {
                 if (result.getBoolean(RESULT_KEY)) {
@@ -226,8 +215,15 @@ public abstract class ProviderApiBase extends IntentService {
         }
     }
 
+    protected void resetProviderDetails() {
+        CA_CERT_DOWNLOADED = PROVIDER_JSON_DOWNLOADED = false;
+        deleteProviderDetailsFromPreferences(providerDefinition);
+        providerCaCert = "";
+        providerDefinition = new JSONObject();
+    }
+
     protected String formatErrorMessage(final int toastStringId) {
-        return formatErrorMessage(getResources().getString(toastStringId));
+        return formatErrorMessage(resources.getString(toastStringId));
     }
 
     private String formatErrorMessage(String errorMessage) {
@@ -243,100 +239,24 @@ public abstract class ProviderApiBase extends IntentService {
         }
     }
 
-    private JSONObject getErrorMessageAsJson(String message) {
+    protected void addErrorMessageToJson(JSONObject jsonObject, String errorMessage) {
         try {
-            return new JSONObject(formatErrorMessage(message));
+            jsonObject.put(ERRORS, errorMessage);
         } catch (JSONException e) {
             e.printStackTrace();
-            return new JSONObject();
         }
     }
-    private OkHttpClient initHttpClient(JSONObject initError, boolean isSelfSigned) {
-        try {
-            TLSCompatSocketFactory sslCompatFactory;
-            ConnectionSpec spec = getConnectionSpec();
-            OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder();
-            if (isSelfSigned) {
-                sslCompatFactory = new TLSCompatSocketFactory(preferences.getString(Provider.CA_CERT, ""));
 
-            } else {
-                sslCompatFactory = new TLSCompatSocketFactory();
-            }
-            sslCompatFactory.initSSLSocketFactory(clientBuilder);
-            clientBuilder.cookieJar(getCookieJar())
-                    .connectionSpecs(Collections.singletonList(spec));
-            return clientBuilder.build();
-        } catch (IllegalStateException e) {
-            e.printStackTrace();
-            initError = getErrorMessageAsJson(String.format(resources.getString(keyChainAccessError), e.getLocalizedMessage()));
-        } catch (KeyStoreException e) {
-            e.printStackTrace();
-            initError = getErrorMessageAsJson(String.format(resources.getString(keyChainAccessError), e.getLocalizedMessage()));
-        } catch (KeyManagementException e) {
-            e.printStackTrace();
-            initError = getErrorMessageAsJson(String.format(resources.getString(keyChainAccessError), e.getLocalizedMessage()));
-        } catch (NoSuchAlgorithmException e) {
-            e.printStackTrace();
-            initError = getErrorMessageAsJson(resources.getString(error_no_such_algorithm_exception_user_message));
-        } catch (CertificateException e) {
-            e.printStackTrace();
-            initError = getErrorMessageAsJson(resources.getString(certificate_error));
-        } catch (UnknownHostException e) {
-            e.printStackTrace();
-            initError = getErrorMessageAsJson(resources.getString(server_unreachable_message));
-        } catch (IOException e) {
-            e.printStackTrace();
-            initError = getErrorMessageAsJson(resources.getString(error_io_exception_user_message));
-        } catch (NoSuchProviderException e) {
+    protected void addErrorMessageToJson(JSONObject jsonObject, String errorMessage, String errorId) {
+        try {
+            jsonObject.put(ERRORS, errorMessage);
+            jsonObject.put(ERRORID, errorId);
+        } catch (JSONException e) {
             e.printStackTrace();
-            initError = getErrorMessageAsJson(resources.getString(error_no_such_algorithm_exception_user_message));
         }
-        return null;
-    }
-
-    protected OkHttpClient initCommercialCAHttpClient(JSONObject initError) {
-        return initHttpClient(initError, false);
-    }
-
-    protected OkHttpClient initSelfSignedCAHttpClient(JSONObject initError) {
-        return initHttpClient(initError, true);
     }
 
-    @NonNull
-    private ConnectionSpec getConnectionSpec() {
-        ConnectionSpec.Builder connectionSpecbuilder = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
-                .tlsVersions(TlsVersion.TLS_1_2, TlsVersion.TLS_1_3);
-        //FIXME: restrict connection further to the following recommended cipher suites for ALL supported API levels
-        //figure out how to use bcjsse for that purpose
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1)
-            connectionSpecbuilder.cipherSuites(
-                    CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
-                    CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
-                    CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
-                    CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
-                    CipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
-                    CipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
-            );
-        return connectionSpecbuilder.build();
-    }
 
-    @NonNull
-    private CookieJar getCookieJar() {
-        return new CookieJar() {
-            private final HashMap<String, List<Cookie>> cookieStore = new HashMap<>();
-
-            @Override
-            public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
-                cookieStore.put(url.host(), cookies);
-            }
-
-            @Override
-            public List<Cookie> loadForRequest(HttpUrl url) {
-                List<Cookie> cookies = cookieStore.get(url.host());
-                return cookies != null ? cookies : new ArrayList<Cookie>();
-            }
-        };
-    }
 
 
     private Bundle tryToRegister(Bundle task) {
@@ -366,7 +286,7 @@ public abstract class ProviderApiBase extends IntentService {
 
     private Bundle register(String username, String password) {
         JSONObject stepResult = null;
-        OkHttpClient okHttpClient = initSelfSignedCAHttpClient(stepResult);
+        OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(stepResult);
         if (okHttpClient == null) {
             return authFailedNotification(stepResult, username);
         }
@@ -376,7 +296,7 @@ public abstract class ProviderApiBase extends IntentService {
 
         BigInteger password_verifier = client.calculateV(username, password, salt);
 
-        JSONObject api_result = sendNewUserDataToSRPServer(provider_api_url, username, new BigInteger(1, salt).toString(16), password_verifier.toString(16), okHttpClient);
+        JSONObject api_result = sendNewUserDataToSRPServer(providerApiUrl, username, new BigInteger(1, salt).toString(16), password_verifier.toString(16), okHttpClient);
 
         Bundle result = new Bundle();
         if (api_result.has(ERRORS))
@@ -423,7 +343,7 @@ public abstract class ProviderApiBase extends IntentService {
     private Bundle authenticate(String username, String password) {
         Bundle result = new Bundle();
         JSONObject stepResult = new JSONObject();
-        OkHttpClient okHttpClient = initSelfSignedCAHttpClient(stepResult);
+        OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(stepResult);
         if (okHttpClient == null) {
             return authFailedNotification(stepResult, username);
         }
@@ -431,13 +351,13 @@ public abstract class ProviderApiBase extends IntentService {
         LeapSRPSession client = new LeapSRPSession(username, password);
         byte[] A = client.exponential();
 
-        JSONObject step_result = sendAToSRPServer(provider_api_url, username, new BigInteger(1, A).toString(16), okHttpClient);
+        JSONObject step_result = sendAToSRPServer(providerApiUrl, username, new BigInteger(1, A).toString(16), okHttpClient);
         try {
             String salt = step_result.getString(LeapSRPSession.SALT);
             byte[] Bbytes = new BigInteger(step_result.getString("B"), 16).toByteArray();
             byte[] M1 = client.response(new BigInteger(salt, 16).toByteArray(), Bbytes);
             if (M1 != null) {
-                step_result = sendM1ToSRPServer(provider_api_url, username, M1, okHttpClient);
+                step_result = sendM1ToSRPServer(providerApiUrl, username, M1, okHttpClient);
                 setTokenIfAvailable(step_result);
                 byte[] M2 = new BigInteger(step_result.getString(LeapSRPSession.M2), 16).toByteArray();
                 if (client.verify(M2)) {
@@ -461,7 +381,7 @@ public abstract class ProviderApiBase extends IntentService {
     private boolean setTokenIfAvailable(JSONObject authentication_step_result) {
         try {
             LeapSRPSession.setToken(authentication_step_result.getString(LeapSRPSession.TOKEN));
-        } catch (JSONException e) { //
+        } catch (JSONException e) {
             return false;
         }
         return true;
@@ -508,7 +428,8 @@ public abstract class ProviderApiBase extends IntentService {
         intentUpdate.setAction(UPDATE_PROGRESSBAR);
         intentUpdate.addCategory(Intent.CATEGORY_DEFAULT);
         intentUpdate.putExtra(CURRENT_PROGRESS, progress);
-        sendBroadcast(intentUpdate);
+        serviceCallback.broadcastProgress(intentUpdate);
+        //sendBroadcast(intentUpdate);
     }
 
     /**
@@ -589,13 +510,13 @@ public abstract class ProviderApiBase extends IntentService {
         return requestJsonFromServer(url, request_method, jsonString, null, okHttpClient);
     }
 
-    protected String sendGetStringToServer(String url, List<Pair<String, String>> headerArgs, OkHttpClient okHttpClient) {
+    protected String sendGetStringToServer(@NonNull String url, @NonNull List<Pair<String, String>> headerArgs, @NonNull OkHttpClient okHttpClient) {
         return requestStringFromServer(url, "GET", null, headerArgs, okHttpClient);
     }
 
 
 
-    private JSONObject requestJsonFromServer(String url, String request_method, String jsonString, List<Pair<String, String>> headerArgs, @NonNull OkHttpClient okHttpClient)  {
+    private JSONObject requestJsonFromServer(@NonNull String url, @NonNull String request_method, String jsonString, @NonNull List<Pair<String, String>> headerArgs, @NonNull OkHttpClient okHttpClient)  {
         JSONObject responseJson;
         String plain_response = requestStringFromServer(url, request_method, jsonString, headerArgs, okHttpClient);
 
@@ -609,41 +530,19 @@ public abstract class ProviderApiBase extends IntentService {
 
     }
 
-    private String requestStringFromServer(String url, String request_method, String jsonString, List<Pair<String, String>> headerArgs, @NonNull OkHttpClient okHttpClient) {
-        Response response;
+    private String requestStringFromServer(@NonNull String url, @NonNull String request_method, String jsonString, @NonNull List<Pair<String, String>> headerArgs, @NonNull OkHttpClient okHttpClient) {
         String plainResponseBody = null;
 
-        RequestBody jsonBody = jsonString != null ? RequestBody.create(JSON, jsonString) : null;
-        Request.Builder requestBuilder = new Request.Builder()
-                .url(url)
-                .method(request_method, jsonBody);
-        if (headerArgs != null) {
-            for (Pair<String, String> keyValPair : headerArgs) {
-                requestBuilder.addHeader(keyValPair.first, keyValPair.second);
-            }
-        }
-        //TODO: move to getHeaderArgs()?
-        String locale = Locale.getDefault().getLanguage() + Locale.getDefault().getCountry();
-        requestBuilder.addHeader("Accept-Language", locale);
-        Request request = requestBuilder.build();
-
         try {
-            response = okHttpClient.newCall(request).execute();
 
-            InputStream inputStream = response.body().byteStream();
-            Scanner scanner = new Scanner(inputStream).useDelimiter("\\A");
-            if (scanner.hasNext()) {
-                plainResponseBody = scanner.next();
-            }
+            plainResponseBody = ProviderApiConnector.requestStringFromServer(url, request_method, jsonString, headerArgs, okHttpClient);
 
         } catch (NullPointerException npe) {
             plainResponseBody = formatErrorMessage(error_json_exception_user_message);
-        } catch (UnknownHostException e) {
+        } catch (UnknownHostException | SocketTimeoutException e) {
             plainResponseBody = formatErrorMessage(server_unreachable_message);
         } catch (MalformedURLException e) {
             plainResponseBody = formatErrorMessage(malformed_url);
-        } catch (SocketTimeoutException e) {
-            plainResponseBody = formatErrorMessage(server_unreachable_message);
         } catch (SSLHandshakeException e) {
             plainResponseBody = formatErrorMessage(certificate_error);
         } catch (ConnectException e) {
@@ -660,6 +559,39 @@ public abstract class ProviderApiBase extends IntentService {
         return plainResponseBody;
     }
 
+    private boolean canConnect(String caCert, JSONObject providerDefinition, Bundle result) {
+        JSONObject errorJson = new JSONObject();
+        String baseUrl = getApiUrl(providerDefinition);
+
+        OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(errorJson, caCert);
+        if (okHttpClient == null) {
+            result.putString(ERRORS, errorJson.toString());
+            return false;
+        }
+
+        try {
+
+            return ProviderApiConnector.canConnect(okHttpClient, baseUrl);
+
+        }  catch (UnknownHostException | SocketTimeoutException e) {
+            setErrorResult(result, server_unreachable_message, null);
+        } catch (MalformedURLException e) {
+            setErrorResult(result, malformed_url, null);
+        } catch (SSLHandshakeException e) {
+            setErrorResult(result, warning_corrupted_provider_cert, ERROR_INVALID_CERTIFICATE.toString());
+        } catch (ConnectException e) {
+            setErrorResult(result, service_is_down_error, null);
+        } catch (IllegalArgumentException e) {
+            setErrorResult(result, error_no_such_algorithm_exception_user_message, null);
+        } catch (UnknownServiceException e) {
+            //unable to find acceptable protocols - tlsv1.2 not enabled?
+            setErrorResult(result, error_no_such_algorithm_exception_user_message, null);
+        } catch (IOException e) {
+            setErrorResult(result, error_io_exception_user_message, null);
+        }
+        return false;
+    }
+
     /**
      * Downloads a provider.json from a given URL, adding a new provider using the given name.
      *
@@ -693,6 +625,7 @@ public abstract class ProviderApiBase extends IntentService {
         } catch(JSONException e) {
             return false;
         } catch(NullPointerException e) {
+            e.printStackTrace();
             return false;
         }
     }
@@ -707,18 +640,12 @@ public abstract class ProviderApiBase extends IntentService {
                     String fingerprint = provider_json.getString(Provider.CA_CERT_FINGERPRINT);
                     String encoding = fingerprint.split(":")[0];
                     String expected_fingerprint = fingerprint.split(":")[1];
-                    String real_fingerprint = base64toHex(Base64.encodeToString(
-                            MessageDigest.getInstance(encoding).digest(certificate.getEncoded()),
-                            Base64.DEFAULT));
+                    String real_fingerprint = getFingerprintFromCertificate(certificate, encoding);
 
                     result = real_fingerprint.trim().equalsIgnoreCase(expected_fingerprint.trim());
                 } else
                     result = false;
-            } catch (JSONException e) {
-                result = false;
-            } catch (NoSuchAlgorithmException e) {
-                result = false;
-            } catch (CertificateEncodingException e) {
+            } catch (JSONException | NoSuchAlgorithmException | CertificateEncodingException e) {
                 result = false;
             }
         }
@@ -726,16 +653,194 @@ public abstract class ProviderApiBase extends IntentService {
         return result;
     }
 
-    private String base64toHex(String base64_input) {
-        byte[] byteArray = Base64.decode(base64_input, Base64.DEFAULT);
-        int readBytes = byteArray.length;
-        StringBuffer hexData = new StringBuffer();
-        int onebyte;
-        for (int i = 0; i < readBytes; i++) {
-            onebyte = ((0x000000ff & byteArray[i]) | 0xffffff00);
-            hexData.append(Integer.toHexString(onebyte).substring(6));
+    protected void checkPersistedProviderUpdates() {
+        String providerDomain = getDomainFromMainURL(lastProviderMainUrl);
+        if (hasUpdatedProviderDetails(providerDomain)) {
+            providerCaCert = getPersistedProviderCA(providerDomain);
+            providerDefinition = getPersistedProviderDefinition(providerDomain);
+            providerCaCertFingerprint = getPersistedCaCertFingerprint(providerDomain);
+            providerApiUrl = getApiUrlWithVersion(providerDefinition);
         }
-        return hexData.toString();
+    }
+
+    protected Bundle validateProviderDetails() {
+        Bundle result = validateCertificateForProvider(providerCaCert, providerDefinition, lastProviderMainUrl);
+
+        //invalid certificate or no certificate
+        if (result.containsKey(ERRORS) || (result.containsKey(RESULT_KEY) && !result.getBoolean(RESULT_KEY)) ) {
+            return result;
+        }
+
+        //valid certificate: skip download, save loaded provider CA cert and provider definition directly
+        try {
+            preferences.edit().putString(Provider.KEY, providerDefinition.toString()).
+                    putBoolean(Constants.PROVIDER_ALLOW_ANONYMOUS, providerDefinition.getJSONObject(Provider.SERVICE).getBoolean(Constants.PROVIDER_ALLOW_ANONYMOUS)).
+                    putBoolean(Constants.PROVIDER_ALLOWED_REGISTERED, providerDefinition.getJSONObject(Provider.SERVICE).getBoolean(Constants.PROVIDER_ALLOWED_REGISTERED)).
+                    putString(Provider.CA_CERT, providerCaCert).commit();
+            CA_CERT_DOWNLOADED = true;
+            PROVIDER_JSON_DOWNLOADED = true;
+            result.putBoolean(RESULT_KEY, true);
+        } catch (JSONException e) {
+            e.printStackTrace();
+            setErrorResult(result, warning_corrupted_provider_details, ERROR_CORRUPTED_PROVIDER_JSON.toString());
+        }
+
+        return result;
+    }
+
+    protected Bundle validateCertificateForProvider(String cert_string, JSONObject providerDefinition, String mainUrl) {
+        Bundle result = new Bundle();
+        result.putBoolean(RESULT_KEY, false);
+
+        if (ConfigHelper.checkErroneousDownload(cert_string)) {
+            return result;
+        }
+
+        X509Certificate certificate = ConfigHelper.parseX509CertificateFromString(cert_string);
+        if (certificate == null) {
+            return setErrorResult(result, warning_corrupted_provider_cert, ERROR_INVALID_CERTIFICATE.toString());
+        }
+        try {
+            certificate.checkValidity();
+            String fingerprint = getCaCertFingerprint(providerDefinition);
+            String encoding = fingerprint.split(":")[0];
+            String expected_fingerprint = fingerprint.split(":")[1];
+            String real_fingerprint = getFingerprintFromCertificate(certificate, encoding);
+            if (!real_fingerprint.trim().equalsIgnoreCase(expected_fingerprint.trim())) {
+                return setErrorResult(result, warning_corrupted_provider_cert, ERROR_CERTIFICATE_PINNING.toString());
+            }
+
+
+            if (!hasApiUrlExpectedDomain(providerDefinition, mainUrl)){
+                return setErrorResult(result, warning_corrupted_provider_details, ERROR_CORRUPTED_PROVIDER_JSON.toString());
+            }
+
+            if (!canConnect(cert_string, providerDefinition, result)) {
+                return result;
+            }
+        } catch (NoSuchAlgorithmException e ) {
+            return setErrorResult(result, error_no_such_algorithm_exception_user_message, null);
+        } catch (ArrayIndexOutOfBoundsException e) {
+            return setErrorResult(result, warning_corrupted_provider_details, ERROR_CORRUPTED_PROVIDER_JSON.toString());
+        } catch (CertificateEncodingException | CertificateNotYetValidException | CertificateExpiredException e) {
+            return setErrorResult(result, warning_expired_provider_cert, ERROR_INVALID_CERTIFICATE.toString());
+        }
+
+        result.putBoolean(RESULT_KEY, true);
+        return result;
+    }
+
+    protected Bundle setErrorResult(Bundle result, int errorMessageId, String errorId) {
+        JSONObject errorJson = new JSONObject();
+        if (errorId != null) {
+            addErrorMessageToJson(errorJson, resources.getString(errorMessageId), errorId);
+        } else {
+            addErrorMessageToJson(errorJson, resources.getString(errorMessageId));
+        }
+        result.putString(ERRORS, errorJson.toString());
+        result.putBoolean(RESULT_KEY, false);
+        return result;
+    }
+
+    /**
+     * This method aims to prevent attacks where the provider.json file got manipulated by a third party.
+     * The main url is visible to the provider when setting up a new provider.
+     * The user is responsible to check that this is the provider main url he intends to connect to.
+     *
+     * @param providerDefinition
+     * @param mainUrlString
+     * @return
+     */
+    private boolean hasApiUrlExpectedDomain(JSONObject providerDefinition, String mainUrlString) {
+        //  fix against "api_uri": "https://calyx.net.malicious.url.net:4430",
+        String apiUrlString = getApiUrl(providerDefinition);
+        String providerDomain = getProviderDomain(providerDefinition);
+        if (mainUrlString.contains(providerDomain) && apiUrlString.contains(providerDomain  + ":")) {
+            return true;
+        }
+        return false;
+    }
+
+    protected String getCaCertFingerprint(JSONObject providerDefinition) {
+        try {
+            return providerDefinition.getString(Provider.CA_CERT_FINGERPRINT);
+        } catch (JSONException e) {
+            e.printStackTrace();
+        }
+        return "";
+    }
+
+    protected String getApiUrl(JSONObject providerDefinition) {
+        try {
+            return providerDefinition.getString(Provider.API_URL);
+        } catch (JSONException e) {
+            e.printStackTrace();
+        }
+        return "";
+    }
+
+    protected String getApiUrlWithVersion(JSONObject providerDefinition) {
+        try {
+            return providerDefinition.getString(Provider.API_URL) + "/" + providerDefinition.getString(Provider.API_VERSION);
+        } catch (JSONException e) {
+            e.printStackTrace();
+        }
+        return "";
+    }
+
+    protected void deleteProviderDetailsFromPreferences(JSONObject providerDefinition) {
+        String providerDomain = getProviderDomain(providerDefinition);
+
+        if (preferences.contains(Provider.KEY + "." + providerDomain)) {
+            preferences.edit().remove(Provider.KEY + "." + providerDomain).apply();
+        }
+        if (preferences.contains(Provider.CA_CERT + "." + providerDomain)) {
+            preferences.edit().remove(Provider.CA_CERT + "." + providerDomain).apply();
+        }
+        if (preferences.contains(Provider.CA_CERT_FINGERPRINT + "." + providerDomain)) {
+            preferences.edit().remove(Provider.CA_CERT_FINGERPRINT + "." + providerDomain).apply();
+        }
+    }
+
+    protected String getPersistedCaCertFingerprint(String providerDomain) {
+        try {
+            return getPersistedProviderDefinition(providerDomain).getString(Provider.CA_CERT_FINGERPRINT);
+        } catch (JSONException e) {
+            e.printStackTrace();
+        }
+        return "";
+    }
+
+    protected JSONObject getPersistedProviderDefinition(String providerDomain) {
+        try {
+            return new JSONObject(preferences.getString(Provider.KEY + "." + providerDomain, ""));
+        } catch (JSONException e) {
+            e.printStackTrace();
+            return new JSONObject();
+        }
+    }
+
+    protected String getPersistedProviderCA(String providerDomain) {
+        return preferences.getString(Provider.CA_CERT + "." + providerDomain, "");
+    }
+
+    protected String getProviderDomain(JSONObject providerDefinition) {
+        try {
+            return providerDefinition.getString(Provider.DOMAIN);
+        } catch (JSONException e) {
+            e.printStackTrace();
+        }
+
+        return "";
+    }
+
+    protected boolean hasUpdatedProviderDetails(String domain) {
+        return preferences.contains(Provider.KEY + "." + domain) && preferences.contains(Provider.CA_CERT + "." + domain);
+    }
+
+    protected String getDomainFromMainURL(String mainUrl) {
+        return mainUrl.replaceFirst("http[s]?://", "").replaceFirst("/.*", "");
+
     }
 
     /**
@@ -753,6 +858,8 @@ public abstract class ProviderApiBase extends IntentService {
         } catch (JSONException e) {
             // TODO Auto-generated catch block
             error_message = string_json_error_message;
+        } catch (NullPointerException e) {
+            //do nothing
         }
 
         return error_message;
@@ -769,31 +876,20 @@ public abstract class ProviderApiBase extends IntentService {
     }
 
     private boolean logOut() {
-        OkHttpClient okHttpClient = initSelfSignedCAHttpClient(new JSONObject());
+        OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(new JSONObject());
         if (okHttpClient == null) {
             return false;
         }
 
-        String deleteUrl = provider_api_url + "/logout";
+        String deleteUrl = providerApiUrl + "/logout";
         int progress = 0;
 
-        Request.Builder requestBuilder = new Request.Builder()
-                .url(deleteUrl)
-                .delete();
-        Request request = requestBuilder.build();
-
-        try {
-            Response response = okHttpClient.newCall(request).execute();
-                                            // v---- was already not authorized
-            if (response.isSuccessful() || response.code() == 401) {
-                broadcastProgress(progress++);
-                LeapSRPSession.setToken("");
-            }
-
-        } catch (IOException e) {
-            return false;
+        if (ProviderApiConnector.delete(okHttpClient, deleteUrl)) {
+            broadcastProgress(progress++);
+            LeapSRPSession.setToken("");
+            return true;
         }
-        return true;
+        return false;
     }
 
     //FIXME: don't save private keys in shared preferences! use the keystore
diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderManager.java b/app/src/main/java/se/leap/bitmaskclient/ProviderManager.java
index abbdeb66a849739477b35f016a0b457b2374a567..92d5da9faeffa2c2039c81946b9df10bf6968416 100644
--- a/app/src/main/java/se/leap/bitmaskclient/ProviderManager.java
+++ b/app/src/main/java/se/leap/bitmaskclient/ProviderManager.java
@@ -1,20 +1,31 @@
 package se.leap.bitmaskclient;
 
-import android.content.res.*;
-
-import com.pedrogomez.renderers.*;
-
-import org.json.*;
-
-import java.io.*;
-import java.net.*;
-import java.util.*;
+import android.content.res.AssetManager;
+
+import com.pedrogomez.renderers.AdapteeCollection;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
 
 /**
  * Created by parmegv on 4/12/14.
  */
 public class ProviderManager implements AdapteeCollection<Provider> {
 
+    private static final String TAG = ProviderManager.class.getName();
     private AssetManager assets_manager;
     private File external_files_dir;
     private Set<Provider> default_providers;
@@ -47,19 +58,27 @@ public class ProviderManager implements AdapteeCollection<Provider> {
 
     private Set<Provider> providersFromAssets(String directory, String[] relative_file_paths) {
         Set<Provider> providers = new HashSet<Provider>();
-        try {
+
             for (String file : relative_file_paths) {
-                InputStream provider_file = assets_manager.open(directory + "/" + file);
-                String main_url = extractMainUrlFromInputStream(provider_file);
-                String certificate_pin = extractCertificatePinFromInputStream(provider_file);
-                if(certificate_pin.isEmpty())
-                    providers.add(new Provider(new URL(main_url)));
-                else
-                    providers.add(new Provider(new URL(main_url), certificate_pin));
+                String mainUrl = null;
+                String certificate = null;
+                String providerDefinition = null;
+                try {
+                    String provider = file.substring(0, file.length() - ".url".length());
+                    InputStream provider_file = assets_manager.open(directory + "/" + file);
+                    mainUrl = extractMainUrlFromInputStream(provider_file);
+                    certificate = ConfigHelper.loadInputStreamAsString(assets_manager.open(provider + ".pem"));
+                    providerDefinition = ConfigHelper.loadInputStreamAsString(assets_manager.open(provider + ".json"));
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+                try {
+                    providers.add(new Provider(new URL(mainUrl), certificate, providerDefinition));
+                } catch (MalformedURLException e) {
+                    e.printStackTrace();
+                }
             }
-        } catch (IOException e) {
-            e.printStackTrace();
-        }
+
         return providers;
     }
 
@@ -89,21 +108,11 @@ public class ProviderManager implements AdapteeCollection<Provider> {
         String main_url = "";
 
         JSONObject file_contents = inputStreamToJson(input_stream);
-        if(file_contents != null)
+        if (file_contents != null)
             main_url = file_contents.optString(Provider.MAIN_URL);
         return main_url;
     }
 
-    private String extractCertificatePinFromInputStream(InputStream input_stream) {
-        String certificate_pin = "";
-
-        JSONObject file_contents = inputStreamToJson(input_stream);
-        if(file_contents != null)
-            certificate_pin = file_contents.optString(Provider.CA_CERT_FINGERPRINT);
-
-        return certificate_pin;
-    }
-
     private JSONObject inputStreamToJson(InputStream input_stream) {
         JSONObject json = null;
         try {
diff --git a/app/src/main/java/se/leap/bitmaskclient/StartActivity.java b/app/src/main/java/se/leap/bitmaskclient/StartActivity.java
index dd2be212169438114094c14debcd4f93aee562da..2bfe650ab754951949fe85ed5b48b43c9e5cfad0 100644
--- a/app/src/main/java/se/leap/bitmaskclient/StartActivity.java
+++ b/app/src/main/java/se/leap/bitmaskclient/StartActivity.java
@@ -62,7 +62,7 @@ public class StartActivity extends Activity {
         }
 
         // initialize app necessities
-        ProviderAPICommand.initialize(this);
+        ProviderAPICommand.initialize(getApplicationContext());
         VpnStatus.initLogCache(getApplicationContext().getCacheDir());
         User.init(getString(R.string.default_username));
 
diff --git a/app/src/main/java/se/leap/bitmaskclient/userstatus/SessionDialog.java b/app/src/main/java/se/leap/bitmaskclient/userstatus/SessionDialog.java
index 613494906bca33251dd0699059f85193c4b0d6be..bd9324bbfe0d8f1895b173aa591d242c7d0023e1 100644
--- a/app/src/main/java/se/leap/bitmaskclient/userstatus/SessionDialog.java
+++ b/app/src/main/java/se/leap/bitmaskclient/userstatus/SessionDialog.java
@@ -23,8 +23,6 @@ import android.view.*;
 import android.widget.*;
 
 import butterknife.*;
-import se.leap.bitmaskclient.ProviderAPI;
-import se.leap.bitmaskclient.VpnFragment;
 import se.leap.bitmaskclient.Provider;
 import se.leap.bitmaskclient.R;
 
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 70f6f4ab2835328866d8e18ef426c1e1f0309753..f59acd3d3b349c1baf9bacc5aaf414414963d9fa 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -82,4 +82,9 @@
     <string name="void_vpn_error_establish">Failed to establish blocking VPN.</string>
     <string name="void_vpn_stopped">Stopped blocking all outgoing internet traffic.</string>
     <string name="void_vpn_title">Blocking traffic</string>
+    <string name="update_provider_details">Update provider details</string>
+    <string name="update_certificate">Update certificate</string>
+    <string name="warning_corrupted_provider_details">Stored provider details are corrupted. You can either update Bitmask (recommended) or update the provider details using a commercial CA certificate.</string>
+    <string name="warning_corrupted_provider_cert">Stored provider certificate is invalid. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate.</string>
+    <string name="warning_expired_provider_cert">Stored provider certificate is expired. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate.</string>
 </resources>
diff --git a/app/src/production/java/se/leap/bitmaskclient/ConfigurationWizard.java b/app/src/production/java/se/leap/bitmaskclient/ConfigurationWizard.java
index fc2569b2309a3ad0629fd18f2140966dfc10fd09..3f05b0a2525637ec7360210a4d8b3c77f0db892e 100644
--- a/app/src/production/java/se/leap/bitmaskclient/ConfigurationWizard.java
+++ b/app/src/production/java/se/leap/bitmaskclient/ConfigurationWizard.java
@@ -63,8 +63,16 @@ public class ConfigurationWizard extends BaseConfigurationWizard {
         mConfigState.setAction(SETTING_UP_PROVIDER);
         Intent provider_API_command = new Intent(this, ProviderAPI.class);
         Bundle parameters = new Bundle();
-        parameters.putString(Provider.MAIN_URL, selected_provider.mainUrl().toString());
-        parameters.putString(Provider.CA_CERT_FINGERPRINT, selected_provider.certificatePin());
+        parameters.putString(Provider.MAIN_URL, selected_provider.getMainUrl().toString());
+        if (selected_provider.hasCertificatePin()){
+            parameters.putString(Provider.CA_CERT_FINGERPRINT, selected_provider.certificatePin());
+        }
+        if (selected_provider.hasCaCert()) {
+            parameters.putString(Provider.CA_CERT, selected_provider.getCaCert());
+        }
+        if (selected_provider.hasDefinition()) {
+            parameters.putString(Provider.KEY, selected_provider.getDefinition().toString());
+        }
 
         provider_API_command.setAction(ProviderAPI.SET_UP_PROVIDER);
         provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters);
@@ -73,15 +81,22 @@ public class ConfigurationWizard extends BaseConfigurationWizard {
         startService(provider_API_command);
     }
 
+    @Override
     public void retrySetUpProvider() {
         cancelSettingUpProvider();
         if (!ProviderAPI.caCertDownloaded()) {
             addAndSelectNewProvider(ProviderAPI.lastProviderMainUrl());
         } else {
-            Intent provider_API_command = new Intent(this, ProviderAPI.class);
+            showProgressBar();
+            adapter.hideAllBut(adapter.indexOf(selected_provider));
+
 
+            Intent provider_API_command = new Intent(this, ProviderAPI.class);
             provider_API_command.setAction(ProviderAPI.SET_UP_PROVIDER);
             provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, providerAPI_result_receiver);
+            Bundle parameters = new Bundle();
+            parameters.putString(Provider.MAIN_URL, selected_provider.getMainUrl().toString());
+            provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters);
 
             startService(provider_API_command);
         }
diff --git a/app/src/production/java/se/leap/bitmaskclient/ProviderAPI.java b/app/src/production/java/se/leap/bitmaskclient/ProviderApiManager.java
similarity index 52%
rename from app/src/production/java/se/leap/bitmaskclient/ProviderAPI.java
rename to app/src/production/java/se/leap/bitmaskclient/ProviderApiManager.java
index fadb03c308c6c2cc3a7247fff60a873fd5f7e5a4..b20a7759e3a154699fb0dc9f2dca93bef5dff7ed 100644
--- a/app/src/production/java/se/leap/bitmaskclient/ProviderAPI.java
+++ b/app/src/production/java/se/leap/bitmaskclient/ProviderApiManager.java
@@ -1,6 +1,5 @@
-
 /**
- * Copyright (c) 2013 LEAP Encryption Access Project and contributers
+ * Copyright (c) 2018 LEAP Encryption Access Project and contributers
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -15,41 +14,49 @@
  * You should have received a copy of the GNU General Public License
  * along with this program. If not, see <http://www.gnu.org/licenses/>.
  */
+
 package se.leap.bitmaskclient;
 
+import android.content.SharedPreferences;
+import android.content.res.Resources;
 import android.os.Bundle;
 import android.util.Pair;
 
 import org.json.JSONException;
 import org.json.JSONObject;
-import org.thoughtcrime.ssl.pinning.util.PinningHelper;
 
 import java.io.IOException;
 import java.net.URL;
 import java.util.List;
-import java.util.Scanner;
-
-import javax.net.ssl.HttpsURLConnection;
-import javax.net.ssl.SSLHandshakeException;
 
 import okhttp3.OkHttpClient;
 import se.leap.bitmaskclient.eip.EIP;
 
-import static se.leap.bitmaskclient.R.string.error_io_exception_user_message;
+import static android.text.TextUtils.isEmpty;
+import static se.leap.bitmaskclient.DownloadFailedDialog.DOWNLOAD_ERRORS.ERROR_CERTIFICATE_PINNING;
+import static se.leap.bitmaskclient.ProviderAPI.ERRORS;
+import static se.leap.bitmaskclient.ProviderAPI.RESULT_KEY;
 import static se.leap.bitmaskclient.R.string.malformed_url;
+import static se.leap.bitmaskclient.R.string.warning_corrupted_provider_cert;
 
 /**
- * Implements HTTP api methods used to manage communications with the provider server.
- * It extends the abstract ProviderApiBase and implements the diverging method calls between the different flavors
- * of ProviderAPI.
- * <p/>
- * It extends an  IntentService because it downloads data from the Internet, so it operates in the background.
- *
- * @author parmegv
- * @author MeanderingCode
- * @author cyberta
+ * Implements the logic of the provider api http requests. The methods of this class need to be called from
+ * a background thread.
  */
-public class ProviderAPI extends ProviderApiBase {
+
+
+public class ProviderApiManager extends ProviderApiManagerBase {
+
+    public ProviderApiManager(SharedPreferences preferences, Resources resources, OkHttpClientGenerator clientGenerator, ProviderApiServiceCallback callback) {
+        super(preferences, resources, clientGenerator, callback);
+    }
+
+    /**
+     * Only used in insecure flavor.
+     */
+    static boolean lastDangerOn() {
+        return false;
+    }
 
     /**
      * Downloads a provider.json from a given URL, adding a new provider using the given name.
@@ -60,72 +67,108 @@ public class ProviderAPI extends ProviderApiBase {
     @Override
     protected Bundle setUpProvider(Bundle task) {
         int progress = 0;
-        Bundle current_download = new Bundle();
+        Bundle currentDownload = new Bundle();
 
-        if (task != null && task.containsKey(Provider.MAIN_URL)) {
-            last_provider_main_url = task.containsKey(Provider.MAIN_URL) ?
+        if (task != null) {
+            //FIXME: this should be refactored in order to avoid static variables all over here
+            lastProviderMainUrl = task.containsKey(Provider.MAIN_URL) ?
                     task.getString(Provider.MAIN_URL) :
                     "";
-            provider_ca_cert_fingerprint = task.containsKey(Provider.CA_CERT_FINGERPRINT) ?
+
+            if (isEmpty(lastProviderMainUrl)) {
+                currentDownload.putBoolean(RESULT_KEY, false);
+                setErrorResult(currentDownload, malformed_url, null);
+                return currentDownload;
+            }
+
+            //TODO: remove that
+            providerCaCertFingerprint = task.containsKey(Provider.CA_CERT_FINGERPRINT) ?
                     task.getString(Provider.CA_CERT_FINGERPRINT) :
                     "";
+            providerCaCert = task.containsKey(Provider.CA_CERT) ?
+                    task.getString(Provider.CA_CERT) :
+                    "";
 
-            CA_CERT_DOWNLOADED = PROVIDER_JSON_DOWNLOADED = EIP_SERVICE_JSON_DOWNLOADED = false;
+            try {
+                providerDefinition = task.containsKey(Provider.KEY) ?
+                        new JSONObject(task.getString(Provider.KEY)) :
+                        new JSONObject();
+            } catch (JSONException e) {
+                e.printStackTrace();
+                providerDefinition = new JSONObject();
+            }
+            providerApiUrl = getApiUrlWithVersion(providerDefinition);
+
+            checkPersistedProviderUpdates();
+            currentDownload = validateProviderDetails();
+
+            //provider details invalid
+            if (currentDownload.containsKey(ERRORS)) {
+                return currentDownload;
+            }
+
+            //no provider certificate available
+            if (currentDownload.containsKey(RESULT_KEY) && !currentDownload.getBoolean(RESULT_KEY)) {
+                resetProviderDetails();
+            }
+
+            EIP_SERVICE_JSON_DOWNLOADED = false;
             go_ahead = true;
         }
 
         if (!PROVIDER_JSON_DOWNLOADED)
-            current_download = getAndSetProviderJson(last_provider_main_url, provider_ca_cert_fingerprint);
-        if (PROVIDER_JSON_DOWNLOADED || (current_download.containsKey(RESULT_KEY) && current_download.getBoolean(RESULT_KEY))) {
+            currentDownload = getAndSetProviderJson(lastProviderMainUrl, providerCaCert, providerDefinition);
+        if (PROVIDER_JSON_DOWNLOADED || (currentDownload.containsKey(RESULT_KEY) && currentDownload.getBoolean(RESULT_KEY))) {
             broadcastProgress(progress++);
             PROVIDER_JSON_DOWNLOADED = true;
 
             if (!CA_CERT_DOWNLOADED)
-                current_download = downloadCACert();
-            if (CA_CERT_DOWNLOADED || (current_download.containsKey(RESULT_KEY) && current_download.getBoolean(RESULT_KEY))) {
+                currentDownload = downloadCACert();
+            if (CA_CERT_DOWNLOADED || (currentDownload.containsKey(RESULT_KEY) && currentDownload.getBoolean(RESULT_KEY))) {
                 broadcastProgress(progress++);
                 CA_CERT_DOWNLOADED = true;
-                current_download = getAndSetEipServiceJson();
-                if (current_download.containsKey(RESULT_KEY) && current_download.getBoolean(RESULT_KEY)) {
+                currentDownload = getAndSetEipServiceJson();
+                if (currentDownload.containsKey(RESULT_KEY) && currentDownload.getBoolean(RESULT_KEY)) {
                     broadcastProgress(progress++);
                     EIP_SERVICE_JSON_DOWNLOADED = true;
                 }
             }
         }
 
-        return current_download;
+        return currentDownload;
     }
 
-    private Bundle getAndSetProviderJson(String provider_main_url, String provider_ca_cert_fingerprint) {
+
+    private Bundle getAndSetProviderJson(String providerMainUrl, String caCert, JSONObject providerDefinition) {
         Bundle result = new Bundle();
 
         if (go_ahead) {
-            String provider_dot_json_string;
-            if(provider_ca_cert_fingerprint.isEmpty())
-                provider_dot_json_string = downloadWithCommercialCA(provider_main_url + "/provider.json");
-            else
-                provider_dot_json_string = downloadWithCommercialCA(provider_main_url + "/provider.json", provider_ca_cert_fingerprint);
+            String providerDotJsonString;
+            if(providerDefinition.length() == 0 || caCert.isEmpty())
+                providerDotJsonString = downloadWithCommercialCA(providerMainUrl + "/provider.json");
+            else {
+                providerDotJsonString = downloadFromApiUrlWithProviderCA("/provider.json", caCert, providerDefinition);
+            }
 
-                if (!isValidJson(provider_dot_json_string)) {
-                    result.putString(ERRORS, getString(malformed_url));
-                    result.putBoolean(RESULT_KEY, false);
-                    return result;
-                }
+            if (!isValidJson(providerDotJsonString)) {
+                setErrorResult(result, malformed_url, null);
+                return result;
+            }
 
             try {
-                JSONObject provider_json = new JSONObject(provider_dot_json_string);
-                provider_api_url = provider_json.getString(Provider.API_URL) + "/" + provider_json.getString(Provider.API_VERSION);
-                String name = provider_json.getString(Provider.NAME);
+                JSONObject providerJson = new JSONObject(providerDotJsonString);
+                String providerDomain = getDomainFromMainURL(lastProviderMainUrl);
+                providerApiUrl = getApiUrlWithVersion(providerJson);
+                //String name = providerJson.getString(Provider.NAME);
                 //TODO setProviderName(name);
 
-                preferences.edit().putString(Provider.KEY, provider_json.toString()).commit();
-                preferences.edit().putBoolean(Constants.PROVIDER_ALLOW_ANONYMOUS, provider_json.getJSONObject(Provider.SERVICE).getBoolean(Constants.PROVIDER_ALLOW_ANONYMOUS)).commit();
-                preferences.edit().putBoolean(Constants.PROVIDER_ALLOWED_REGISTERED, provider_json.getJSONObject(Provider.SERVICE).getBoolean(Constants.PROVIDER_ALLOWED_REGISTERED)).commit();
-
+                preferences.edit().putString(Provider.KEY, providerJson.toString()).
+                        putBoolean(Constants.PROVIDER_ALLOW_ANONYMOUS, providerJson.getJSONObject(Provider.SERVICE).getBoolean(Constants.PROVIDER_ALLOW_ANONYMOUS)).
+                        putBoolean(Constants.PROVIDER_ALLOWED_REGISTERED, providerJson.getJSONObject(Provider.SERVICE).getBoolean(Constants.PROVIDER_ALLOWED_REGISTERED)).
+                        putString(Provider.KEY + "." + providerDomain, providerJson.toString()).commit();
                 result.putBoolean(RESULT_KEY, true);
             } catch (JSONException e) {
-                //TODO Error message should be contained in that provider_dot_json_string
-                String reason_to_fail = pickErrorMessage(provider_dot_json_string);
+                String reason_to_fail = pickErrorMessage(providerDotJsonString);
                 result.putString(ERRORS, reason_to_fail);
                 result.putBoolean(RESULT_KEY, false);
             }
@@ -176,7 +219,7 @@ public class ProviderAPI extends ProviderApiBase {
 
             String cert_string = downloadWithProviderCA(new_cert_string_url.toString());
 
-            if (cert_string == null || cert_string.isEmpty() || ConfigHelper.checkErroneousDownload(cert_string))
+            if (ConfigHelper.checkErroneousDownload(cert_string))
                 return false;
             else
                 return loadCertificate(cert_string);
@@ -194,46 +237,20 @@ public class ProviderAPI extends ProviderApiBase {
     private Bundle downloadCACert() {
         Bundle result = new Bundle();
         try {
-            JSONObject provider_json = new JSONObject(preferences.getString(Provider.KEY, ""));
-            String ca_cert_url = provider_json.getString(Provider.CA_CERT_URI);
-            String cert_string = downloadWithCommercialCA(ca_cert_url);
-            result.putBoolean(RESULT_KEY, true);
+            JSONObject providerJson = new JSONObject(preferences.getString(Provider.KEY, ""));
+            String caCertUrl = providerJson.getString(Provider.CA_CERT_URI);
+            String providerDomain = getDomainFromMainURL(lastProviderMainUrl);
+            String cert_string = downloadWithCommercialCA(caCertUrl);
 
             if (validCertificate(cert_string) && go_ahead) {
                 preferences.edit().putString(Provider.CA_CERT, cert_string).commit();
+                preferences.edit().putString(Provider.CA_CERT + "." + providerDomain, cert_string).commit();
                 result.putBoolean(RESULT_KEY, true);
             } else {
-                String reason_to_fail = pickErrorMessage(cert_string);
-                result.putString(ERRORS, reason_to_fail);
-                result.putBoolean(RESULT_KEY, false);
+                setErrorResult(result, warning_corrupted_provider_cert, ERROR_CERTIFICATE_PINNING.toString());
             }
         } catch (JSONException e) {
-            String reason_to_fail = formatErrorMessage(malformed_url);
-            result.putString(ERRORS, reason_to_fail);
-            result.putBoolean(RESULT_KEY, false);
-        }
-
-        return result;
-    }
-
-    //TODO: refactor with ticket #8773
-    private String downloadWithCommercialCA(String url_string, String ca_cert_fingerprint) {
-        String result = "";
-
-        int seconds_of_timeout = 2;
-        String[] pins = new String[] {ca_cert_fingerprint};
-        try {
-            URL url = new URL(url_string);
-            HttpsURLConnection connection = PinningHelper.getPinnedHttpsURLConnection(Dashboard.getContext(), pins, url);
-            connection.setConnectTimeout(seconds_of_timeout * 1000);
-            if (!LeapSRPSession.getToken().isEmpty())
-                connection.addRequestProperty(LeapSRPSession.AUTHORIZATION_HEADER, "Token token=" + LeapSRPSession.getToken());
-            result = new Scanner(connection.getInputStream()).useDelimiter("\\A").next();
-        } catch (IOException e) {
-            if(e instanceof SSLHandshakeException)
-                result = formatErrorMessage(R.string.error_security_pinnedcertificate);
-            else
-                result = formatErrorMessage(error_io_exception_user_message);
+            setErrorResult(result, malformed_url, null);
         }
 
         return result;
@@ -245,11 +262,11 @@ public class ProviderAPI extends ProviderApiBase {
      * @param string_url
      * @return
      */
-    protected String downloadWithCommercialCA(String string_url) {
+    private String downloadWithCommercialCA(String string_url) {
         String responseString;
         JSONObject errorJson = new JSONObject();
 
-        OkHttpClient okHttpClient = initCommercialCAHttpClient(errorJson);
+        OkHttpClient okHttpClient = clientGenerator.initCommercialCAHttpClient(errorJson);
         if (okHttpClient == null) {
             return errorJson.toString();
         }
@@ -262,7 +279,7 @@ public class ProviderAPI extends ProviderApiBase {
             try {
                 // try to download with provider CA on certificate error
                 JSONObject responseErrorJson = new JSONObject(responseString);
-                if (responseErrorJson.getString(ERRORS).equals(getString(R.string.certificate_error))) {
+                if (responseErrorJson.getString(ERRORS).equals(resources.getString(R.string.certificate_error))) {
                     responseString = downloadWithProviderCA(string_url);
                 }
             } catch (JSONException e) {
@@ -273,17 +290,41 @@ public class ProviderAPI extends ProviderApiBase {
         return responseString;
     }
 
+
+    /**
+     * Tries to download the contents of the provided url using not commercially validated CA certificate from chosen provider.
+     *
+     * @return an empty string if it fails, the response body if not.
+     */
+    private String downloadFromApiUrlWithProviderCA(String path, String caCert, JSONObject providerDefinition) {
+        String responseString;
+        JSONObject errorJson = new JSONObject();
+        String baseUrl = getApiUrl(providerDefinition);
+        OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(errorJson, caCert);
+        if (okHttpClient == null) {
+            return errorJson.toString();
+        }
+
+        String urlString = baseUrl + path;
+        List<Pair<String, String>> headerArgs = getAuthorizationHeader();
+        responseString = sendGetStringToServer(urlString, headerArgs, okHttpClient);
+
+        return responseString;
+
+    }
+
+
     /**
      * Tries to download the contents of the provided url using not commercially validated CA certificate from chosen provider.
      *
      * @param urlString as a string
      * @return an empty string if it fails, the url content if not.
      */
-    protected String downloadWithProviderCA(String urlString) {
+    private String downloadWithProviderCA(String urlString) {
         JSONObject initError = new JSONObject();
         String responseString;
 
-        OkHttpClient okHttpClient = initSelfSignedCAHttpClient(initError);
+        OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(initError);
         if (okHttpClient == null) {
             return initError.toString();
         }
@@ -294,5 +335,4 @@ public class ProviderAPI extends ProviderApiBase {
 
         return responseString;
     }
-
 }
diff --git a/app/src/test/java/se/leap/bitmaskclient/TestUtils.java b/app/src/test/java/se/leap/bitmaskclient/TestUtils.java
deleted file mode 100644
index 96b11df6aa9d78449eddc5568d91c9a71f220f0c..0000000000000000000000000000000000000000
--- a/app/src/test/java/se/leap/bitmaskclient/TestUtils.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package se.leap.bitmaskclient;
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-
-/**
- * Created by cyberta on 08.10.17.
- */
-
-public class TestUtils {
-
-    public static String getInputAsString(InputStream fileAsInputStream) throws IOException {
-        BufferedReader br = new BufferedReader(new InputStreamReader(fileAsInputStream));
-        StringBuilder sb = new StringBuilder();
-        String line = br.readLine();
-        while (line != null) {
-            sb.append(line);
-            line = br.readLine();
-        }
-
-        return sb.toString();
-    }
-
-}
diff --git a/app/src/test/java/se/leap/bitmaskclient/eip/GatewaysManagerTest.java b/app/src/test/java/se/leap/bitmaskclient/eip/GatewaysManagerTest.java
index ea2124802619262f5100e0e55420e6518f843f88..4726cab7c198e74b34d49da8b53eca9f6d368493 100644
--- a/app/src/test/java/se/leap/bitmaskclient/eip/GatewaysManagerTest.java
+++ b/app/src/test/java/se/leap/bitmaskclient/eip/GatewaysManagerTest.java
@@ -16,7 +16,7 @@ import java.io.IOException;
 
 import se.leap.bitmaskclient.Constants;
 import se.leap.bitmaskclient.Provider;
-import se.leap.bitmaskclient.TestUtils;
+import se.leap.bitmaskclient.testutils.TestSetupHelper;
 
 import static junit.framework.Assert.assertEquals;
 import static org.mockito.ArgumentMatchers.anyInt;
@@ -61,17 +61,17 @@ public class GatewaysManagerTest {
 
     @Test
     public void testFromEipServiceJson_ignoreDuplicateGateways() throws Exception {
-        String eipServiceJson = TestUtils.getInputAsString(getClass().getClassLoader().getResourceAsStream("eip-service-two-gateways.json"));
+        String eipServiceJson = TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("eip-service-two-gateways.json"));
         gatewaysManager.fromEipServiceJson(new JSONObject(eipServiceJson));
         assertEquals(2, gatewaysManager.size());
-        eipServiceJson = TestUtils.getInputAsString(getClass().getClassLoader().getResourceAsStream("eip-service-one-gateway.json"));
+        eipServiceJson = TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("eip-service-one-gateway.json"));
         gatewaysManager.fromEipServiceJson(new JSONObject(eipServiceJson));
         assertEquals(2, gatewaysManager.size());
     }
 
     @Test
     public void testClearGatewaysAndProfiles_resetGateways() throws Exception {
-        String eipServiceJson = TestUtils.getInputAsString(getClass().getClassLoader().getResourceAsStream("eip-service-two-gateways.json"));
+        String eipServiceJson = TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("eip-service-two-gateways.json"));
         gatewaysManager.fromEipServiceJson(new JSONObject(eipServiceJson));
         assertEquals(2, gatewaysManager.size());
         gatewaysManager.clearGatewaysAndProfiles();
@@ -79,7 +79,7 @@ public class GatewaysManagerTest {
     }
 
     private String getJsonStringFor(String filename) throws IOException {
-        return TestUtils.getInputAsString(getClass().getClassLoader().getResourceAsStream(filename));
+        return TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream(filename));
     }
 
 }
\ No newline at end of file
diff --git a/app/src/test/java/se/leap/bitmaskclient/eip/ProviderApiManagerTest.java b/app/src/test/java/se/leap/bitmaskclient/eip/ProviderApiManagerTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..9ca90b17f3df47855c86ffe5d48415691cf6bf33
--- /dev/null
+++ b/app/src/test/java/se/leap/bitmaskclient/eip/ProviderApiManagerTest.java
@@ -0,0 +1,337 @@
+/**
+ * Copyright (c) 2018 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package se.leap.bitmaskclient.eip;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.text.TextUtils;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Answers;
+import org.mockito.Mock;
+import org.powermock.api.mockito.PowerMockito;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+import java.io.IOException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateEncodingException;
+
+import se.leap.bitmaskclient.ConfigHelper;
+import se.leap.bitmaskclient.Provider;
+import se.leap.bitmaskclient.ProviderAPI;
+import se.leap.bitmaskclient.ProviderApiConnector;
+import se.leap.bitmaskclient.ProviderApiManager;
+import se.leap.bitmaskclient.ProviderApiManagerBase;
+import se.leap.bitmaskclient.testutils.MockSharedPreferences;
+
+import static se.leap.bitmaskclient.ProviderAPI.ERRORS;
+import static se.leap.bitmaskclient.ProviderAPI.PROVIDER_NOK;
+import static se.leap.bitmaskclient.ProviderAPI.PROVIDER_OK;
+import static se.leap.bitmaskclient.ProviderAPI.RESULT_KEY;
+import static se.leap.bitmaskclient.testutils.BackendMockResponses.BackendMockProvider.TestBackendErrorCase.ERROR_CASE_UPDATED_CERTIFICATE;
+import static se.leap.bitmaskclient.testutils.BackendMockResponses.BackendMockProvider.TestBackendErrorCase.NO_ERROR;
+import static se.leap.bitmaskclient.testutils.TestSetupHelper.getInputAsString;
+import static se.leap.bitmaskclient.testutils.TestSetupHelper.mockBundle;
+import static se.leap.bitmaskclient.testutils.TestSetupHelper.mockClientGenerator;
+import static se.leap.bitmaskclient.testutils.TestSetupHelper.mockFingerprintForCertificate;
+import static se.leap.bitmaskclient.testutils.TestSetupHelper.mockIntent;
+import static se.leap.bitmaskclient.testutils.TestSetupHelper.mockProviderApiConnector;
+import static se.leap.bitmaskclient.testutils.TestSetupHelper.mockResources;
+import static se.leap.bitmaskclient.testutils.TestSetupHelper.mockResultReceiver;
+import static se.leap.bitmaskclient.testutils.TestSetupHelper.mockTextUtils;
+
+
+/**
+ * Created by cyberta on 04.01.18.
+ */
+
+@RunWith(PowerMockRunner.class)
+@PrepareForTest({ProviderApiManager.class, TextUtils.class, ConfigHelper.class, ProviderApiConnector.class})
+public class ProviderApiManagerTest {
+
+    private SharedPreferences mockPreferences;
+    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+    private Resources mockResources;
+    @Mock(answer =  Answers.RETURNS_DEEP_STUBS)
+    private Context mockContext;
+
+    private ProviderApiManager providerApiManager;
+
+    class TestProviderApiServiceCallback implements ProviderApiManagerBase.ProviderApiServiceCallback {
+
+        //Intent expectedIntent;
+        TestProviderApiServiceCallback(/*Intent expectedIntent*/) {
+            //this.expectedIntent = expectedIntent;
+        }
+
+        @Override
+        public void broadcastProgress(Intent intent) {
+            //assertEquals("expected intent: ", expectedIntent, intent);
+        }
+    }
+
+    @Before
+    public void setUp() throws Exception {
+
+        Bundle bundle = mockBundle();
+        PowerMockito.whenNew(Bundle.class).withAnyArguments().thenReturn(bundle);
+        Intent intent = mockIntent();
+        PowerMockito.whenNew(Intent.class).withAnyArguments().thenReturn(intent);
+        mockTextUtils();
+        mockPreferences = new MockSharedPreferences();
+        mockResources = mockResources(getClass().getClassLoader().getResourceAsStream("error_messages.json"));
+    }
+
+
+    @Test
+    public void test_handleIntentSetupProvider_noProviderMainURL() {
+        providerApiManager = new ProviderApiManager(mockPreferences, mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+        Bundle expectedResult = mockBundle();
+        expectedResult.putBoolean(RESULT_KEY, false);
+        expectedResult.putString(ERRORS, "{\"errors\":\"It doesn't seem to be a Bitmask provider.\"}");
+
+        Intent provider_API_command = mockIntent();
+        Bundle parameters = mockBundle();
+        parameters.putString(Provider.MAIN_URL, "");
+
+        provider_API_command.setAction(ProviderAPI.SET_UP_PROVIDER);
+        provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters);
+        provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_NOK, expectedResult));
+
+        providerApiManager.handleIntent(provider_API_command);
+    }
+
+    @Test
+    public void test_handleIntentSetupProvider_happyPath_preseededProviderAndCA() throws IOException, CertificateEncodingException, NoSuchAlgorithmException {
+        mockFingerprintForCertificate(" a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494");
+        mockProviderApiConnector(NO_ERROR);
+        providerApiManager = new ProviderApiManager(mockPreferences, mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+        Bundle expectedResult = mockBundle();
+        expectedResult.putBoolean(RESULT_KEY, true);
+
+        Intent provider_API_command = mockIntent();
+        Bundle parameters = mockBundle();
+        parameters.putString(Provider.MAIN_URL, "https://riseup.net");
+        parameters.putString(Provider.CA_CERT, getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.pem")));
+        parameters.putString(Provider.KEY, getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.json")));
+
+        provider_API_command.setAction(ProviderAPI.SET_UP_PROVIDER);
+        provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters);
+        provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_OK, expectedResult));
+
+        providerApiManager.handleIntent(provider_API_command);
+    }
+
+    @Test
+    public void test_handleIntentSetupProvider_happyPath_no_preseededProviderAndCA() throws IOException, CertificateEncodingException, NoSuchAlgorithmException {
+        mockFingerprintForCertificate("a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494");
+        mockProviderApiConnector(NO_ERROR);
+        providerApiManager = new ProviderApiManager(mockPreferences, mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+        Bundle expectedResult = mockBundle();
+        expectedResult.putBoolean(RESULT_KEY, true);
+
+        Intent provider_API_command = mockIntent();
+        Bundle parameters = mockBundle();
+        parameters.putString(Provider.MAIN_URL, "https://riseup.net");
+
+        provider_API_command.setAction(ProviderAPI.SET_UP_PROVIDER);
+        provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters);
+        provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_OK, expectedResult));
+
+        providerApiManager.handleIntent(provider_API_command);
+    }
+
+    @Test
+    public void test_handleIntentSetupProvider_happyPath_storedProviderAndCAFromPreviousSetup() throws IOException, CertificateEncodingException, NoSuchAlgorithmException {
+        mockFingerprintForCertificate("a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494");
+        mockProviderApiConnector(NO_ERROR);
+        mockPreferences.edit().putString(Provider.KEY + ".riseup.net", getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.json"))).apply();
+        mockPreferences.edit().putString(Provider.CA_CERT + ".riseup.net", getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.pem"))).apply();
+        providerApiManager = new ProviderApiManager(mockPreferences, mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+        Bundle expectedResult = mockBundle();
+        expectedResult.putBoolean(RESULT_KEY, true);
+
+        Intent provider_API_command = mockIntent();
+        Bundle parameters = mockBundle();
+        parameters.putString(Provider.MAIN_URL, "https://riseup.net");
+
+        provider_API_command.setAction(ProviderAPI.SET_UP_PROVIDER);
+        provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters);
+        provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_OK, expectedResult));
+
+        providerApiManager.handleIntent(provider_API_command);
+    }
+
+    @Test
+    public void test_handleIntentSetupProvider_preseededProviderAndCA_failedCAPinning() throws IOException, CertificateEncodingException, NoSuchAlgorithmException {
+        mockFingerprintForCertificate(" a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29495");
+        mockProviderApiConnector(NO_ERROR);
+        providerApiManager = new ProviderApiManager(mockPreferences, mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+        Bundle expectedResult = mockBundle();
+        expectedResult.putBoolean(RESULT_KEY, false);
+        expectedResult.putString(ERRORS, "{\"errorId\":\"ERROR_CERTIFICATE_PINNING\",\"errors\":\"Stored provider certificate is invalid. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate.\"}");
+
+        Intent provider_API_command = mockIntent();
+        Bundle parameters = mockBundle();
+        parameters.putString(Provider.MAIN_URL, "https://riseup.net");
+        parameters.putString(Provider.CA_CERT, getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.pem")));
+        parameters.putString(Provider.KEY, getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.json")));
+
+        provider_API_command.setAction(ProviderAPI.SET_UP_PROVIDER);
+        provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters);
+        provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_NOK, expectedResult));
+
+        providerApiManager.handleIntent(provider_API_command);
+    }
+
+    @Test
+    public void test_handleIntentSetupProvider_no_preseededProviderAndCA_failedPinning() throws IOException, CertificateEncodingException, NoSuchAlgorithmException {
+        mockFingerprintForCertificate("a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29495");
+        mockProviderApiConnector(NO_ERROR);
+        providerApiManager = new ProviderApiManager(mockPreferences, mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+        Bundle expectedResult = mockBundle();
+        expectedResult.putBoolean(RESULT_KEY, false);
+        expectedResult.putString(ERRORS, "{\"errorId\":\"ERROR_CERTIFICATE_PINNING\",\"errors\":\"Stored provider certificate is invalid. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate.\"}");
+
+        Intent provider_API_command = mockIntent();
+        Bundle parameters = mockBundle();
+        parameters.putString(Provider.MAIN_URL, "https://riseup.net");
+
+        provider_API_command.setAction(ProviderAPI.SET_UP_PROVIDER);
+        provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters);
+        provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_NOK, expectedResult));
+
+        providerApiManager.handleIntent(provider_API_command);
+    }
+
+    @Test
+    public void test_handleIntentSetupProvider_storedProviderAndCAFromPreviousSetup_failedPinning() throws IOException, CertificateEncodingException, NoSuchAlgorithmException {
+        mockFingerprintForCertificate("a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29495");
+        mockProviderApiConnector(NO_ERROR);
+        mockPreferences.edit().putString(Provider.KEY + ".riseup.net", getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.json"))).apply();
+        mockPreferences.edit().putString(Provider.CA_CERT + ".riseup.net", getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.pem"))).apply();
+        providerApiManager = new ProviderApiManager(mockPreferences, mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+        Bundle expectedResult = mockBundle();
+        expectedResult.putBoolean(RESULT_KEY, false);
+        expectedResult.putString(ERRORS, "{\"errorId\":\"ERROR_CERTIFICATE_PINNING\",\"errors\":\"Stored provider certificate is invalid. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate.\"}");
+
+        Intent provider_API_command = mockIntent();
+        Bundle parameters = mockBundle();
+        parameters.putString(Provider.MAIN_URL, "https://riseup.net");
+
+        provider_API_command.setAction(ProviderAPI.SET_UP_PROVIDER);
+        provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters);
+        provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_NOK, expectedResult));
+
+        providerApiManager.handleIntent(provider_API_command);
+    }
+
+
+    @Test
+    public void test_handleIntentSetupProvider_preseededProviderAndCA_outdatedCertificate() throws IOException, CertificateEncodingException, NoSuchAlgorithmException {
+        mockProviderApiConnector(NO_ERROR);
+        providerApiManager = new ProviderApiManager(mockPreferences, mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+        Bundle expectedResult = mockBundle();
+        expectedResult.putBoolean(RESULT_KEY, false);
+        expectedResult.putString(ERRORS, "{\"errorId\":\"ERROR_INVALID_CERTIFICATE\",\"errors\":\"Stored provider certificate is expired. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate.\"}");
+
+        Intent provider_API_command = mockIntent();
+        Bundle parameters = mockBundle();
+        parameters.putString(Provider.MAIN_URL, "https://riseup.net");
+        parameters.putString(Provider.CA_CERT, getInputAsString(getClass().getClassLoader().getResourceAsStream("outdated_cert.pem")));
+        parameters.putString(Provider.KEY, getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.json")));
+
+        provider_API_command.setAction(ProviderAPI.SET_UP_PROVIDER);
+        provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters);
+        provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_NOK, expectedResult));
+
+        providerApiManager.handleIntent(provider_API_command);
+    }
+
+    @Test
+    public void test_handleIntentSetupProvider_storedProviderAndCAFromPreviousSetup_outdatedCertificate() throws IOException, CertificateEncodingException, NoSuchAlgorithmException {
+        mockProviderApiConnector(NO_ERROR);
+        mockPreferences.edit().putString(Provider.KEY + ".riseup.net", getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.json"))).apply();
+        mockPreferences.edit().putString(Provider.CA_CERT + ".riseup.net", getInputAsString(getClass().getClassLoader().getResourceAsStream("outdated_cert.pem"))).apply();
+        providerApiManager = new ProviderApiManager(mockPreferences, mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+        Bundle expectedResult = mockBundle();
+        expectedResult.putBoolean(RESULT_KEY, false);
+        expectedResult.putString(ERRORS, "{\"errorId\":\"ERROR_INVALID_CERTIFICATE\",\"errors\":\"Stored provider certificate is expired. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate.\"}");
+
+        Intent provider_API_command = mockIntent();
+        Bundle parameters = mockBundle();
+        parameters.putString(Provider.MAIN_URL, "https://riseup.net");
+
+        provider_API_command.setAction(ProviderAPI.SET_UP_PROVIDER);
+        provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters);
+        provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_NOK, expectedResult));
+
+        providerApiManager.handleIntent(provider_API_command);
+    }
+
+    @Test
+    public void test_handleIntentSetupProvider_preseededProviderAndCA_ValidCertificateButUpdatedCertificateOnServerSide() throws IOException, CertificateEncodingException, NoSuchAlgorithmException {
+        mockFingerprintForCertificate(" a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494");
+        mockProviderApiConnector(ERROR_CASE_UPDATED_CERTIFICATE);
+
+        providerApiManager = new ProviderApiManager(mockPreferences, mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+        Bundle expectedResult = mockBundle();
+        expectedResult.putBoolean(RESULT_KEY, false);
+        expectedResult.putString(ERRORS, "{\"errorId\":\"ERROR_INVALID_CERTIFICATE\",\"errors\":\"Stored provider certificate is invalid. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate.\"}");
+
+        Intent provider_API_command = mockIntent();
+        Bundle parameters = mockBundle();
+        parameters.putString(Provider.MAIN_URL, "https://riseup.net");
+        parameters.putString(Provider.CA_CERT, getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.pem")));
+        parameters.putString(Provider.KEY, getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.json")));
+
+        provider_API_command.setAction(ProviderAPI.SET_UP_PROVIDER);
+        provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters);
+        provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_NOK, expectedResult));
+
+        providerApiManager.handleIntent(provider_API_command);
+    }
+
+    @Test
+    public void test_handleIntentSetupProvider_storedProviderAndCAFromPreviousSetup_ValidCertificateButUpdatedCertificateOnServerSide() throws IOException, CertificateEncodingException, NoSuchAlgorithmException {
+        mockFingerprintForCertificate("a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494");
+        mockProviderApiConnector(ERROR_CASE_UPDATED_CERTIFICATE);
+        mockPreferences.edit().putString(Provider.KEY + ".riseup.net", getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.json"))).apply();
+        mockPreferences.edit().putString(Provider.CA_CERT + ".riseup.net", getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.pem"))).apply();
+        providerApiManager = new ProviderApiManager(mockPreferences, mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+        Bundle expectedResult = mockBundle();
+        expectedResult.putBoolean(RESULT_KEY, false);
+        expectedResult.putString(ERRORS, "{\"errorId\":\"ERROR_INVALID_CERTIFICATE\",\"errors\":\"Stored provider certificate is invalid. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate.\"}");
+
+        Intent provider_API_command = mockIntent();
+        Bundle parameters = mockBundle();
+        parameters.putString(Provider.MAIN_URL, "https://riseup.net");
+
+        provider_API_command.setAction(ProviderAPI.SET_UP_PROVIDER);
+        provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters);
+        provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_NOK, expectedResult));
+
+        providerApiManager.handleIntent(provider_API_command);
+    }
+}
diff --git a/app/src/test/java/se/leap/bitmaskclient/eip/VpnConfigGeneratorTest.java b/app/src/test/java/se/leap/bitmaskclient/eip/VpnConfigGeneratorTest.java
index 7e60edb9b90a54cfc2fac800f44e50388f91da6e..8c8cdb6187b76c71c0ab570c153f046358c340b3 100644
--- a/app/src/test/java/se/leap/bitmaskclient/eip/VpnConfigGeneratorTest.java
+++ b/app/src/test/java/se/leap/bitmaskclient/eip/VpnConfigGeneratorTest.java
@@ -4,7 +4,7 @@ import org.json.JSONObject;
 import org.junit.Before;
 import org.junit.Test;
 
-import se.leap.bitmaskclient.TestUtils;
+import se.leap.bitmaskclient.testutils.TestSetupHelper;
 
 import static junit.framework.Assert.assertTrue;
 
@@ -217,13 +217,13 @@ public class VpnConfigGeneratorTest {
 
     @Before
     public void setUp() throws  Exception {
-        generalConfig = new JSONObject(TestUtils.getInputAsString(getClass().getClassLoader().getResourceAsStream("general_configuration.json")));
-        secrets = new JSONObject(TestUtils.getInputAsString(getClass().getClassLoader().getResourceAsStream("secrets.json")));
+        generalConfig = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("general_configuration.json")));
+        secrets = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("secrets.json")));
 
     }
     @Test
     public void testGenerate_tcp_udp() throws Exception {
-        gateway = new JSONObject(TestUtils.getInputAsString(getClass().getClassLoader().getResourceAsStream("gateway_tcp_udp.json")));
+        gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("gateway_tcp_udp.json")));
         vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway);
 
         String vpnConfig = vpnConfigGenerator.generate();
@@ -232,7 +232,7 @@ public class VpnConfigGeneratorTest {
 
     @Test
     public void testGenerate_udp_tcp() throws Exception {
-        gateway = new JSONObject(TestUtils.getInputAsString(getClass().getClassLoader().getResourceAsStream("gateway_udp_tcp.json")));
+        gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("gateway_udp_tcp.json")));
         vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway);
 
         String vpnConfig = vpnConfigGenerator.generate();
diff --git a/app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/BackendMockProvider.java b/app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/BackendMockProvider.java
new file mode 100644
index 0000000000000000000000000000000000000000..9069661fdb795b577eaaebc370ca4d170d46806f
--- /dev/null
+++ b/app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/BackendMockProvider.java
@@ -0,0 +1,86 @@
+/**
+ * Copyright (c) 2018 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package se.leap.bitmaskclient.testutils.BackendMockResponses;
+
+import java.io.IOException;
+
+/**
+ * Created by cyberta on 10.01.18.
+ */
+
+public class BackendMockProvider {
+    /**
+     * This enum can be useful to provide different responses from a mocked ProviderApiConnector
+     * in order to test different error scenarios
+     */
+    public enum TestBackendErrorCase {
+        NO_ERROR,
+        ERROR_CASE_UPDATED_CERTIFICATE,
+        ERROR_NO_RESPONSE_BODY,         // => NullPointerException
+        ERROR_DNS_RESOLUTION_ERROR,     // => UnkownHostException
+        ERROR_SOCKET_TIMEOUT,           // => SocketTimeoutException
+        ERROR_WRONG_PROTOCOL,           // => MalformedURLException
+        ERROR_CERTIFICATE_INVALID,      // => SSLHandshakeException
+        ERROR_WRONG_PORT,               // => ConnectException
+        ERROR_PAYLOAD_MISSING,          // => IllegalArgumentException
+        ERROR_TLS_1_2_NOT_SUPPORTED,    // => UnknownServiceException
+        ERROR_UNKNOWN_IO_EXCEPTION,     // => IOException
+        ERROR_NO_ACCESS,
+        ERROR_INVALID_SESSION_TOKEN,
+        ERROR_NO_CONNECTION,
+        ERROR_WRONG_SRP_CREDENTIALS
+    }
+
+
+    public static void provideBackendResponsesFor(TestBackendErrorCase errorCase) throws IOException {
+        switch (errorCase) {
+
+            case NO_ERROR:
+                new NoErrorBackendResponse();
+                break;
+            case ERROR_CASE_UPDATED_CERTIFICATE:
+                new UpdatedCertificateBackendResponse();
+                break;
+            case ERROR_NO_RESPONSE_BODY:
+                break;
+            case ERROR_DNS_RESOLUTION_ERROR:
+                break;
+            case ERROR_SOCKET_TIMEOUT:
+                break;
+            case ERROR_WRONG_PROTOCOL:
+                break;
+            case ERROR_CERTIFICATE_INVALID:
+                break;
+            case ERROR_WRONG_PORT:
+                break;
+            case ERROR_PAYLOAD_MISSING:
+                break;
+            case ERROR_TLS_1_2_NOT_SUPPORTED:
+                break;
+            case ERROR_UNKNOWN_IO_EXCEPTION:
+                break;
+            case ERROR_NO_ACCESS:
+                break;
+            case ERROR_INVALID_SESSION_TOKEN:
+                break;
+            case ERROR_NO_CONNECTION:
+                break;
+            case ERROR_WRONG_SRP_CREDENTIALS:
+                break;
+        }
+    }
+}
diff --git a/app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/BaseBackendResponse.java b/app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/BaseBackendResponse.java
new file mode 100644
index 0000000000000000000000000000000000000000..9822401941b8cccb456fcea4da75097d15493bd2
--- /dev/null
+++ b/app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/BaseBackendResponse.java
@@ -0,0 +1,75 @@
+/**
+ * Copyright (c) 2018 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package se.leap.bitmaskclient.testutils.BackendMockResponses;
+
+import android.util.Pair;
+
+import org.mockito.ArgumentMatchers;
+import org.mockito.Mockito;
+import org.mockito.stubbing.Answer;
+
+import java.io.IOException;
+
+import okhttp3.OkHttpClient;
+import se.leap.bitmaskclient.ProviderApiConnector;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.powermock.api.mockito.PowerMockito.mockStatic;
+
+/**
+ * Created by cyberta on 10.01.18.
+ */
+
+public abstract class BaseBackendResponse {
+
+    private Answer<String> answerRequestStringFromServer;
+    private Answer<Boolean> answerCanConnect;
+    private Answer<Boolean> answerDelete;
+
+    public BaseBackendResponse() throws IOException {
+        mockStatic(ProviderApiConnector.class);
+        this.answerRequestStringFromServer = getAnswerForRequestStringFromServer();
+        this.answerCanConnect = getAnswerForCanConnect();
+        this.answerDelete = getAnswerForDelete();
+
+        responseOnRequestStringFromServer();
+        responseOnCanConnect();
+        responseOnDelete();
+
+    }
+
+    public abstract Answer<String> getAnswerForRequestStringFromServer();
+    public abstract Answer<Boolean> getAnswerForCanConnect();
+    public abstract Answer<Boolean> getAnswerForDelete();
+
+
+    public void responseOnRequestStringFromServer() throws IOException, RuntimeException {
+        Mockito.when(ProviderApiConnector.requestStringFromServer(anyString(), anyString(), nullable(String.class), ArgumentMatchers.<Pair<String,String>>anyList(), any(OkHttpClient.class))).
+                thenAnswer(answerRequestStringFromServer);
+    }
+
+    public void responseOnCanConnect() throws IOException, RuntimeException {
+        Mockito.when(ProviderApiConnector.canConnect(any(OkHttpClient.class), anyString())).thenAnswer(answerCanConnect);
+    }
+
+    public void responseOnDelete() throws IOException, RuntimeException {
+        Mockito.when(ProviderApiConnector.delete(any(OkHttpClient.class), anyString())).thenAnswer(answerDelete);
+    }
+
+}
diff --git a/app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/NoErrorBackendResponse.java b/app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/NoErrorBackendResponse.java
new file mode 100644
index 0000000000000000000000000000000000000000..fa318e4254f4b54c0c0b0c9a0b4f38037bf97a8e
--- /dev/null
+++ b/app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/NoErrorBackendResponse.java
@@ -0,0 +1,89 @@
+/**
+ * Copyright (c) 2018 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package se.leap.bitmaskclient.testutils.BackendMockResponses;
+
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.io.IOException;
+
+import static se.leap.bitmaskclient.testutils.TestSetupHelper.getInputAsString;
+
+/**
+ * Created by cyberta on 10.01.18.
+ */
+
+public class NoErrorBackendResponse extends BaseBackendResponse {
+    public NoErrorBackendResponse() throws IOException {
+        super();
+    }
+
+    @Override
+    public Answer<String> getAnswerForRequestStringFromServer() {
+        return new Answer<String>() {
+            @Override
+            public String answer(InvocationOnMock invocation) throws Throwable {
+                String url = (String) invocation.getArguments()[0];
+                String requestMethod = (String) invocation.getArguments()[1];
+                String jsonPayload = (String) invocation.getArguments()[2];
+
+                if (url.contains("/provider.json")) {
+                    //download provider json
+                    return getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.json"));
+                } else if (url.contains("/ca.crt")) {
+                    //download provider ca cert
+                    return getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.pem"));
+                } else if (url.contains("config/eip-service.json")) {
+                    // download provider service json containing gateways, locations and openvpn settings
+                    return getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.service.json"));
+                } else if (url.contains("/users.json")) {
+                    //create new user
+                    //TODO: implement me
+                } else if (url.contains("/sessions.json")) {
+                    //srp auth: sendAToSRPServer
+                    //TODO: implement me
+                } else if (url.contains("/sessions/parmegvtest10.json")){
+                    //srp auth: sendM1ToSRPServer
+                    //TODO: implement me
+                }
+
+                return null;
+            }
+        };
+    }
+
+    @Override
+    public Answer<Boolean> getAnswerForCanConnect() {
+        return new Answer<Boolean>() {
+            @Override
+            public Boolean answer(InvocationOnMock invocation) throws Throwable {
+                return true;
+            }
+        };
+    }
+
+    @Override
+    public Answer<Boolean> getAnswerForDelete() {
+        return new Answer<Boolean>() {
+            @Override
+            public Boolean answer(InvocationOnMock invocation) throws Throwable {
+                return true;
+            }
+        };
+    }
+
+}
diff --git a/app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/UpdatedCertificateBackendResponse.java b/app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/UpdatedCertificateBackendResponse.java
new file mode 100644
index 0000000000000000000000000000000000000000..232649a1208bb37875e8f862b3f2899a9f4dabdc
--- /dev/null
+++ b/app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/UpdatedCertificateBackendResponse.java
@@ -0,0 +1,97 @@
+/**
+ * Copyright (c) 2018 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package se.leap.bitmaskclient.testutils.BackendMockResponses;
+
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.io.IOException;
+
+import javax.net.ssl.SSLHandshakeException;
+
+import static se.leap.bitmaskclient.testutils.TestSetupHelper.getInputAsString;
+
+/**
+ * Created by cyberta on 10.01.18.
+ */
+
+public class UpdatedCertificateBackendResponse extends BaseBackendResponse {
+    static volatile boolean wasCACertCalled = false;
+
+
+    public UpdatedCertificateBackendResponse() throws IOException {
+        super();
+    }
+
+    @Override
+    public Answer<String> getAnswerForRequestStringFromServer() {
+        return new Answer<String>() {
+
+            @Override
+            public String answer(InvocationOnMock invocation) throws Throwable {
+                String url = (String) invocation.getArguments()[0];
+
+                if (url.contains("/provider.json")) {
+                    if (!wasCACertCalled) {
+                        throw new SSLHandshakeException("Updated certificate on server side");
+                    }
+                    //download provider json
+                    return getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.json"));
+                } else if (url.contains("/ca.crt")) {
+                    //download provider ca cert
+                    wasCACertCalled = true;
+                    return getInputAsString(getClass().getClassLoader().getResourceAsStream("updated_cert.pem"));
+                } else if (url.contains("config/eip-service.json")) {
+                    // download provider service json containing gateways, locations and openvpn settings
+                    if (!wasCACertCalled) {
+                        throw new SSLHandshakeException("Updated certificate on server side");
+                    }
+                    return getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.service.json"));
+                }
+
+                return null;
+            }
+        };
+    }
+
+    @Override
+    public Answer<Boolean> getAnswerForCanConnect() {
+        return new Answer<Boolean>() {
+            @Override
+            public Boolean answer(InvocationOnMock invocation) throws Throwable {
+                if (!wasCACertCalled) {
+                    throw new SSLHandshakeException("Updated certificate on server side");
+                }
+                return true;
+            }
+        };
+    }
+
+    @Override
+    public Answer<Boolean> getAnswerForDelete() {
+        return new Answer<Boolean>() {
+            @Override
+            public Boolean answer(InvocationOnMock invocation) throws Throwable {
+                if (!wasCACertCalled) {
+                    throw new SSLHandshakeException("Updated certificate on server side");
+                }
+                return true;
+            }
+        };
+    }
+
+}
diff --git a/app/src/test/java/se/leap/bitmaskclient/testutils/MockSharedPreferences.java b/app/src/test/java/se/leap/bitmaskclient/testutils/MockSharedPreferences.java
new file mode 100644
index 0000000000000000000000000000000000000000..af35af310cc848aa834ea4d07e1b58df789210f8
--- /dev/null
+++ b/app/src/test/java/se/leap/bitmaskclient/testutils/MockSharedPreferences.java
@@ -0,0 +1,165 @@
+/**
+ * Copyright (c) 2018 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package se.leap.bitmaskclient.testutils;
+
+import android.content.SharedPreferences;
+import android.support.annotation.Nullable;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Created by cyberta on 09.01.18.
+ */
+
+public class MockSharedPreferences implements SharedPreferences {
+    HashMap<String, String> mockedStringPrefs = new HashMap<>();
+    HashMap<String, Integer> mockedIntPrefs = new HashMap<>();
+    HashMap<String, Boolean> mockedBooleanPrefs = new HashMap<>();
+
+    @Override
+    public Map<String, ?> getAll() {
+        return null;
+    }
+
+    @Nullable
+    @Override
+    public String getString(String key, @Nullable String defValue) {
+        String value = mockedStringPrefs.get(key);
+        return value != null ? value : defValue;
+    }
+
+    @Nullable
+    @Override
+    public Set<String> getStringSet(String key, @Nullable Set<String> defValues) {
+        return null;
+    }
+
+    @Override
+    public int getInt(String key, int defValue) {
+        Integer value = mockedIntPrefs.get(key);
+        return value != null ? value : defValue;
+    }
+
+    @Override
+    public long getLong(String key, long defValue) {
+        return 0;
+    }
+
+    @Override
+    public float getFloat(String key, float defValue) {
+        return 0;
+    }
+
+    @Override
+    public boolean getBoolean(String key, boolean defValue) {
+        Boolean value = mockedBooleanPrefs.get(key);
+        return value != null ? value : defValue;
+    }
+
+    @Override
+    public boolean contains(String key) {
+        return mockedStringPrefs.containsKey(key) ||
+                mockedBooleanPrefs.containsKey(key) ||
+                mockedIntPrefs.containsKey(key);
+    }
+
+    @Override
+    public Editor edit() {
+        return new Editor() {
+            private HashMap<String, String> tempStrings = new HashMap<>(mockedStringPrefs);
+            private HashMap<String, Integer> tempIntegers = new HashMap<>(mockedIntPrefs);
+            private HashMap<String, Boolean> tempBoolean = new HashMap<>(mockedBooleanPrefs);
+
+            @Override
+            public Editor putString(String key, @Nullable String value) {
+                tempStrings.put(key, value);
+                return this;
+            }
+
+            @Override
+            public Editor putStringSet(String key, @Nullable Set<String> values) {
+                return null;
+            }
+
+            @Override
+            public Editor putInt(String key, int value) {
+                tempIntegers.put(key, value);
+                return this;
+            }
+
+            @Override
+            public Editor putLong(String key, long value) {
+                return null;
+            }
+
+            @Override
+            public Editor putFloat(String key, float value) {
+                return null;
+            }
+
+            @Override
+            public Editor putBoolean(String key, boolean value) {
+                tempBoolean.put(key, value);
+                return this;
+            }
+
+            @Override
+            public Editor remove(String key) {
+                tempBoolean.remove(key);
+                tempStrings.remove(key);
+                tempIntegers.remove(key);
+                return this;
+            }
+
+            @Override
+            public Editor clear() {
+                tempBoolean.clear();
+                tempStrings.clear();
+                tempIntegers.clear();
+                return this;
+            }
+
+            @Override
+            public boolean commit() {
+                mockedStringPrefs = new HashMap<>(tempStrings);
+                mockedBooleanPrefs = new HashMap<>(tempBoolean);
+                mockedIntPrefs = new HashMap<>(tempIntegers);
+                return true;
+            }
+
+            @Override
+            public void apply() {
+                mockedStringPrefs = new HashMap<>(tempStrings);
+                mockedBooleanPrefs = new HashMap<>(tempBoolean);
+                mockedIntPrefs = new HashMap<>(tempIntegers);
+            }
+        };
+    }
+
+    @Override
+    public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
+
+    }
+
+    @Override
+    public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
+
+    }
+}
diff --git a/app/src/test/java/se/leap/bitmaskclient/testutils/TestSetupHelper.java b/app/src/test/java/se/leap/bitmaskclient/testutils/TestSetupHelper.java
new file mode 100644
index 0000000000000000000000000000000000000000..f8f70eaf257e8685a62bd85a3e1d732fdbad697c
--- /dev/null
+++ b/app/src/test/java/se/leap/bitmaskclient/testutils/TestSetupHelper.java
@@ -0,0 +1,434 @@
+/**
+ * Copyright (c) 2018 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package se.leap.bitmaskclient.testutils;
+
+import android.content.Intent;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.os.ResultReceiver;
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import okhttp3.OkHttpClient;
+import se.leap.bitmaskclient.ConfigHelper;
+import se.leap.bitmaskclient.OkHttpClientGenerator;
+import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.testutils.BackendMockResponses.BackendMockProvider;
+import se.leap.bitmaskclient.testutils.matchers.BundleMatcher;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.powermock.api.mockito.PowerMockito.mockStatic;
+
+/**
+ * Created by cyberta on 08.10.17.
+ */
+
+public class TestSetupHelper {
+
+
+
+    public static String getInputAsString(InputStream fileAsInputStream) throws IOException {
+        BufferedReader br = new BufferedReader(new InputStreamReader(fileAsInputStream));
+        StringBuilder sb = new StringBuilder();
+        String line = br.readLine();
+        while (line != null) {
+            sb.append(line);
+            line = br.readLine();
+        }
+
+        return sb.toString();
+    }
+
+    @NonNull
+    public static Bundle mockBundle() {
+        final Map<String, Boolean> fakeBooleanBundle = new HashMap<>();
+        final Map<String, String> fakeStringBundle = new HashMap<>();
+        final Map<String, Integer> fakeIntBundle = new HashMap<>();
+        final Map<String, Parcelable> fakeParcelableBundle = new HashMap<>();
+
+        Bundle bundle = mock(Bundle.class);
+
+        //mock String values in Bundle
+        doAnswer(new Answer() {
+            @Override
+            public Object answer(InvocationOnMock invocation) throws Throwable {
+                Object[] arguments = invocation.getArguments();
+                String key = ((String) arguments[0]);
+                String value = ((String) arguments[1]);
+                fakeStringBundle.put(key, value);
+                return null;
+            }
+        }).when(bundle).putString(anyString(), anyString());
+        when(bundle.getString(anyString())).thenAnswer(new Answer<String>() {
+            @Override
+            public String answer(InvocationOnMock invocation) throws Throwable {
+                Object[] arguments = invocation.getArguments();
+                String key = ((String) arguments[0]);
+                return fakeStringBundle.get(key);
+            }
+        });
+
+        //mock Boolean values in Bundle
+        doAnswer(new Answer() {
+            @Override
+            public Object answer(InvocationOnMock invocation) throws Throwable {
+                Object[] arguments = invocation.getArguments();
+                String key = ((String) arguments[0]);
+                Boolean value = ((boolean) arguments[1]);
+                fakeBooleanBundle.put(key, value);
+                return null;
+            }
+        }).when(bundle).putBoolean(anyString(), anyBoolean());
+        when(bundle.getBoolean(anyString())).thenAnswer(new Answer<Boolean>() {
+            @Override
+            public Boolean answer(InvocationOnMock invocation) throws Throwable {
+                Object[] arguments = invocation.getArguments();
+                String key = ((String) arguments[0]);
+                return fakeBooleanBundle.get(key);
+            }
+        });
+
+        //mock Integer values in Bundle
+        doAnswer(new Answer() {
+            @Override
+            public Object answer(InvocationOnMock invocation) throws Throwable {
+                Object[] arguments = invocation.getArguments();
+                String key = ((String) arguments[0]);
+                Integer value = ((int) arguments[1]);
+                fakeIntBundle.put(key, value);
+                return null;
+            }
+        }).when(bundle).putInt(anyString(), anyInt());
+        when(bundle.getInt(anyString())).thenAnswer(new Answer<Integer>() {
+            @Override
+            public Integer answer(InvocationOnMock invocation) throws Throwable {
+                Object[] arguments = invocation.getArguments();
+                String key = ((String) arguments[0]);
+                return fakeIntBundle.get(key);
+            }
+        });
+
+        //mock Parcelable values in Bundle
+        doAnswer(new Answer() {
+            @Override
+            public Object answer(InvocationOnMock invocation) throws Throwable {
+                Object[] arguments = invocation.getArguments();
+                String key = ((String) arguments[0]);
+                Parcelable value = ((Parcelable) arguments[1]);
+                fakeParcelableBundle.put(key, value);
+                return null;
+            }
+        }).when(bundle).putParcelable(anyString(), any(Parcelable.class));
+        when(bundle.getParcelable(anyString())).thenAnswer(new Answer<Parcelable>() {
+            @Override
+            public Parcelable answer(InvocationOnMock invocation) throws Throwable {
+                Object[] arguments = invocation.getArguments();
+                String key = ((String) arguments[0]);
+                return fakeParcelableBundle.get(key);
+            }
+        });
+
+        //mock get
+        when(bundle.get(anyString())).thenAnswer(new Answer<Object>() {
+            @Override
+            public Object answer(InvocationOnMock invocation) throws Throwable {
+                Object[] arguments = invocation.getArguments();
+                String key = ((String) arguments[0]);
+                if (fakeBooleanBundle.containsKey(key)) {
+                    return fakeBooleanBundle.get(key);
+                } else if (fakeIntBundle.containsKey(key)) {
+                    return fakeIntBundle.get(key);
+                } else if (fakeStringBundle.containsKey(key)) {
+                    return fakeStringBundle.get(key);
+                } else {
+                    return fakeParcelableBundle.get(key);
+                }
+            }
+        });
+
+        //mock getKeySet
+       when(bundle.keySet()).thenAnswer(new Answer<Set<String>>() {
+           @Override
+           public Set<String> answer(InvocationOnMock invocation) throws Throwable {
+               //this whole approach as a drawback:
+               //you should not add the same keys for values of different types
+               HashSet<String> keys = new HashSet<String>();
+               keys.addAll(fakeBooleanBundle.keySet());
+               keys.addAll(fakeIntBundle.keySet());
+               keys.addAll(fakeStringBundle.keySet());
+               keys.addAll(fakeParcelableBundle.keySet());
+               return keys;
+           }
+        });
+
+        //mock containsKey
+        when(bundle.containsKey(anyString())).thenAnswer(new Answer<Boolean>() {
+            @Override
+            public Boolean answer(InvocationOnMock invocation) throws Throwable {
+                String key = (String) invocation.getArguments()[0];
+                return fakeBooleanBundle.containsKey(key) ||
+                        fakeStringBundle.containsKey(key) ||
+                        fakeIntBundle.containsKey(key) ||
+                        fakeParcelableBundle.containsKey(key);
+            }
+        });
+
+        return bundle;
+    }
+
+    public static Intent mockIntent() {
+        Intent intent = mock(Intent.class);
+        final String[] action = new String[1];
+        final Map<String, Object> fakeExtras = new HashMap<>();
+        final List<String> categories = new ArrayList<>();
+
+
+        //mock Action in intent
+        doAnswer(new Answer() {
+            @Override
+            public Object answer(InvocationOnMock invocation) throws Throwable {
+                Object[] arguments = invocation.getArguments();
+                action[0] = ((String) arguments[0]);
+                return null;
+            }
+        }).when(intent).setAction(anyString());
+        when(intent.getAction()).thenAnswer(new Answer<String>() {
+            @Override
+            public String answer(InvocationOnMock invocation) throws Throwable {
+                return action[0];
+            }
+        });
+
+        //mock Bundle in intent extras
+        doAnswer(new Answer() {
+            @Override
+            public Object answer(InvocationOnMock invocation) throws Throwable {
+                Object[] arguments = invocation.getArguments();
+                String key = ((String) arguments[0]);
+                Bundle value = ((Bundle) arguments[1]);
+                fakeExtras.put(key, value);
+                return null;
+            }
+        }).when(intent).putExtra(anyString(), any(Bundle.class));
+        when(intent.getBundleExtra(anyString())).thenAnswer(new Answer<Bundle>() {
+            @Override
+            public Bundle answer(InvocationOnMock invocation) throws Throwable {
+                Object[] arguments = invocation.getArguments();
+                String key = ((String) arguments[0]);
+                return (Bundle) fakeExtras.get(key);
+            }
+        });
+
+        //mock Parcelable in intent extras
+        doAnswer(new Answer() {
+            @Override
+            public Object answer(InvocationOnMock invocation) throws Throwable {
+                Object[] arguments = invocation.getArguments();
+                String key = ((String) arguments[0]);
+                Parcelable value = ((Parcelable) arguments[1]);
+                fakeExtras.put(key, value);
+                return null;
+            }
+        }).when(intent).putExtra(anyString(), any(Parcelable.class));
+        when(intent.getParcelableExtra(anyString())).thenAnswer(new Answer<Parcelable>() {
+            @Override
+            public Parcelable answer(InvocationOnMock invocation) throws Throwable {
+                Object[] arguments = invocation.getArguments();
+                String key = ((String) arguments[0]);
+                return (Parcelable) fakeExtras.get(key);
+            }
+        });
+        doAnswer(new Answer() {
+            @Override
+            public Object answer(InvocationOnMock invocation) throws Throwable {
+                Object[] arguments = invocation.getArguments();
+                categories.add(((String) arguments[0]));
+                return null;
+            }
+        }).when(intent).addCategory(anyString());
+
+        when(intent.getCategories()).thenAnswer(new Answer<Set<String>>() {
+            @Override
+            public Set<String> answer(InvocationOnMock invocation) throws Throwable {
+                return new HashSet<>(categories);
+            }
+        });
+
+        return intent;
+    }
+
+    public static void mockTextUtils() {
+        mockStatic(TextUtils.class);
+
+        when(TextUtils.equals(any(CharSequence.class), any(CharSequence.class))).thenAnswer(new Answer<Object>() {
+            @Override
+            public Object answer(InvocationOnMock invocation) throws Throwable {
+                CharSequence a = (CharSequence) invocation.getArguments()[0];
+                CharSequence b = (CharSequence) invocation.getArguments()[1];
+                if (a == b) return true;
+                int length;
+                if (a != null && b != null && (length = a.length()) == b.length()) {
+                    if (a instanceof String && b instanceof String) {
+                        return a.equals(b);
+                    } else {
+                        for (int i = 0; i < length; i++) {
+                            if (a.charAt(i) != b.charAt(i)) return false;
+                        }
+                        return true;
+                    }
+                }
+                return false;
+            }
+        });
+
+        when(TextUtils.isEmpty(any(CharSequence.class))).thenAnswer(new Answer<Boolean>() {
+            @Override
+            public Boolean answer(InvocationOnMock invocation) throws Throwable {
+                CharSequence param = (CharSequence) invocation.getArguments()[0];
+                return param == null || param.length() == 0;
+            }
+        });
+    }
+
+
+    public static ResultReceiver mockResultReceiver(final int expectedResultCode, final Bundle expectedBundle) {
+        ResultReceiver resultReceiver = mock(ResultReceiver.class);
+
+        doAnswer(new Answer() {
+            @Override
+            public Object answer(InvocationOnMock invocation) throws Throwable {
+                Object[] arguments = invocation.getArguments();
+                int resultCode = (int) arguments[0];
+                Bundle bundle = (Bundle) arguments[1];
+                Set<String> keys = expectedBundle.keySet();
+                Iterator<String> iterator = keys.iterator();
+                HashMap<String, Integer> expectedIntegers = new HashMap<>();
+                HashMap<String, String> expectedStrings = new HashMap<>();
+                HashMap<String, Boolean> expectedBooleans = new HashMap<>();
+                HashMap<String, Parcelable> expectedParcelables = new HashMap<>();
+                while (iterator.hasNext()) {
+                    String key = iterator.next();
+                    Object value = expectedBundle.get(key);
+
+                    if (value instanceof Boolean) {
+                        expectedBooleans.put(key, (Boolean) value);
+                    } else if (value instanceof Integer) {
+                        expectedIntegers.put(key, (Integer) value);
+                    } else if (value instanceof String) {
+                        expectedStrings.put(key, (String) value);
+                    } else if (value instanceof Parcelable) {
+                        expectedParcelables.put(key, (Parcelable) value);
+                    }
+                }
+                assertThat("expected bundle: ", bundle, new BundleMatcher(expectedIntegers, expectedStrings, expectedBooleans, expectedParcelables));
+                assertEquals("expected resultCode: ", expectedResultCode, resultCode);
+                return null;
+            }
+        }).when(resultReceiver).send(anyInt(), any(Bundle.class));
+        return resultReceiver;
+    }
+
+    public static void mockFingerprintForCertificate(String mockedFingerprint) throws CertificateEncodingException, NoSuchAlgorithmException {
+        mockStatic(ConfigHelper.class);
+        when(ConfigHelper.getFingerprintFromCertificate(any(X509Certificate.class), anyString())).thenReturn(mockedFingerprint);
+        when(ConfigHelper.checkErroneousDownload(anyString())).thenCallRealMethod();
+        when(ConfigHelper.parseX509CertificateFromString(anyString())).thenCallRealMethod();
+    }
+
+    public static void mockProviderApiConnector(final BackendMockProvider.TestBackendErrorCase errorCase) throws IOException {
+        BackendMockProvider.provideBackendResponsesFor(errorCase);
+    }
+
+    public static OkHttpClientGenerator mockClientGenerator() {
+        OkHttpClientGenerator mockClientGenerator = mock(OkHttpClientGenerator.class);
+        OkHttpClient mockedOkHttpClient = mock(OkHttpClient.class);
+        when(mockClientGenerator.initCommercialCAHttpClient(any(JSONObject.class))).thenReturn(mockedOkHttpClient);
+        when(mockClientGenerator.initSelfSignedCAHttpClient(any(JSONObject.class))).thenReturn(mockedOkHttpClient);
+        when(mockClientGenerator.initSelfSignedCAHttpClient(any(JSONObject.class), anyString())).thenReturn(mockedOkHttpClient);
+        return mockClientGenerator;
+    }
+
+    public static Resources mockResources(InputStream inputStream) throws IOException, JSONException {
+        Resources mockedResources = mock(Resources.class, RETURNS_DEEP_STUBS);
+        JSONObject errorMessages = new JSONObject(getInputAsString(inputStream));
+
+
+        when(mockedResources.getString(R.string.warning_corrupted_provider_details)).
+                thenReturn(errorMessages.getString("warning_corrupted_provider_details"));
+        when(mockedResources.getString(R.string.server_unreachable_message)).
+                thenReturn(errorMessages.getString("server_unreachable_message"));
+        when(mockedResources.getString(R.string.error_security_pinnedcertificate)).
+                thenReturn(errorMessages.getString("error.security.pinnedcertificate"));
+        when(mockedResources.getString(R.string.malformed_url)).
+                thenReturn(errorMessages.getString("malformed_url"));
+        when(mockedResources.getString(R.string.certificate_error)).
+                thenReturn(errorMessages.getString("certificate_error"));
+        when(mockedResources.getString(R.string.error_srp_math_error_user_message)).
+                thenReturn(errorMessages.getString("error_srp_math_error_user_message"));
+        when(mockedResources.getString(R.string.error_bad_user_password_user_message)).
+                thenReturn(errorMessages.getString("error_bad_user_password_user_message"));
+        when(mockedResources.getString(R.string.error_not_valid_password_user_message)).
+                thenReturn(errorMessages.getString("error_not_valid_password_user_message"));
+        when(mockedResources.getString(R.string.error_client_http_user_message)).
+                thenReturn(errorMessages.getString("error_client_http_user_message"));
+        when(mockedResources.getString(R.string.error_io_exception_user_message)).
+                thenReturn(errorMessages.getString("error_io_exception_user_message"));
+        when(mockedResources.getString(R.string.error_json_exception_user_message)).
+                thenReturn(errorMessages.getString("error_json_exception_user_message"));
+        when(mockedResources.getString(R.string.error_no_such_algorithm_exception_user_message)).
+                thenReturn(errorMessages.getString("error_no_such_algorithm_exception_user_message"));
+        when(mockedResources.getString(R.string.warning_corrupted_provider_details)).
+                thenReturn(errorMessages.getString("warning_corrupted_provider_details"));
+        when(mockedResources.getString(R.string.warning_corrupted_provider_cert)).
+                thenReturn(errorMessages.getString("warning_corrupted_provider_cert"));
+        when(mockedResources.getString(R.string.warning_expired_provider_cert)).
+                thenReturn(errorMessages.getString("warning_expired_provider_cert"));
+        return mockedResources;
+    }
+
+}
diff --git a/app/src/test/java/se/leap/bitmaskclient/testutils/matchers/BundleMatcher.java b/app/src/test/java/se/leap/bitmaskclient/testutils/matchers/BundleMatcher.java
new file mode 100644
index 0000000000000000000000000000000000000000..d2d2a102c02258e473bc0d9ed91457194292aba0
--- /dev/null
+++ b/app/src/test/java/se/leap/bitmaskclient/testutils/matchers/BundleMatcher.java
@@ -0,0 +1,208 @@
+/**
+ * Copyright (c) 2018 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package se.leap.bitmaskclient.testutils.matchers;
+
+import android.os.Bundle;
+import android.os.Parcelable;
+
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Set;
+
+/**
+ * Created by cyberta on 09.01.18.
+ */
+
+public class BundleMatcher extends BaseMatcher<Bundle> {
+
+    private final HashMap<String, Integer> expectedIntegers;
+    private final HashMap<String, String> expectedStrings;
+    private final HashMap<String, Boolean> expectedBooleans;
+    private final HashMap<String, Parcelable> expectedParcelables;
+    private HashMap<String, Integer> unfoundExpectedInteger  = new HashMap<>();
+    private HashMap<String, Boolean> unfoundExpectedBoolean = new HashMap<>();
+    private HashMap<String, String> unfoundExpectedString = new HashMap<>();
+    private HashMap<String, Parcelable> unfoundExpectedParcelable = new HashMap<>();
+    private HashMap<String, Object> unexpectedAdditionalObjects = new HashMap<>();
+
+    public BundleMatcher(HashMap<String, Integer> expectedIntegers, HashMap<String, String> expectedStrings, HashMap<String, Boolean> expectedBooleans, HashMap<String, Parcelable> expectedParcelables) {
+        this.expectedBooleans = expectedBooleans;
+        this.expectedIntegers = expectedIntegers;
+        this.expectedStrings = expectedStrings;
+        this.expectedParcelables = expectedParcelables;
+    }
+
+    @Override
+    public boolean matches(Object item) {
+        if (item instanceof Bundle) {
+            Bundle actualBundle = (Bundle) item;
+            return checkActualBundleHasAllExpectedBooleanValues(actualBundle) &&
+                    checkActualBundleHasAllExpectedStringValues(actualBundle) &&
+                    checkActualBundleHasAllExpectedIntValues(actualBundle) &&
+                    checkActualBundleHasAllExpectedParcelableValues(actualBundle) &&
+                    checkUnexpectedAdditionalValuesIn(actualBundle);
+        }
+        return false;
+    }
+
+    @Override
+    public void describeTo(Description description) {
+        description.appendText("Bundle didn't match expectation!");
+
+        if (!unfoundExpectedInteger.isEmpty()) {
+            Iterator<String> iterator = unfoundExpectedInteger.keySet().iterator();
+            while (iterator.hasNext()) {
+                String key = iterator.next();
+                if (unfoundExpectedInteger.get(key) == null) {
+                    description.appendText("\n unfound Integer in actual Bundle: ").appendValue(iterator.next());
+                } else {
+                    description.appendText("\n expected Integer for key " + key).appendValue(expectedIntegers.get(key)).
+                            appendText("\n found Integer was: ").appendValue(unfoundExpectedInteger.get(key));
+                }
+            }
+        }
+        if (!unfoundExpectedBoolean.isEmpty()) {
+            Iterator<String> iterator = unfoundExpectedBoolean.keySet().iterator();
+            while (iterator.hasNext()) {
+                String key = iterator.next();
+                if (unfoundExpectedBoolean.get(key) == null) {
+                    description.appendText("\n unfound Boolean in actual Bundle: ").appendValue(iterator.next());
+                } else {
+                    description.appendText("\n expected Boolean for key " + key).appendValue(expectedBooleans.get(key)).
+                            appendText("\n found Boolean was: ").appendValue(unfoundExpectedBoolean.get(key));
+                }
+            }
+        }
+        if (!unfoundExpectedString.isEmpty()) {
+            Iterator<String> iterator = unfoundExpectedString.keySet().iterator();
+            while (iterator.hasNext()) {
+                String key = iterator.next();
+                if (unfoundExpectedString.get(key) == null) {
+                    description.appendText("\n unfound String in actual Bundle: ").appendValue(iterator.next());
+                } else {
+                    description.appendText("\n expected String for key " + key).appendValue(expectedStrings.get(key)).
+                            appendText("\n found String was: ").appendValue(unfoundExpectedString.get(key));
+                }
+            }
+        }
+        if (!unfoundExpectedParcelable.isEmpty()) {
+            Iterator<String> iterator = unfoundExpectedInteger.keySet().iterator();
+            while (iterator.hasNext()) {
+                String key = iterator.next();
+                if (unfoundExpectedParcelable.get(key) == null) {
+                    description.appendText("\n unfound Parcelable in actual Bundle: ").appendValue(iterator.next());
+                } else {
+                    description.appendText("\n expected Parcelable or key " + key).appendValue(expectedParcelables.get(key)).
+                            appendText("\n found Parcelable was: ").appendValue(unfoundExpectedParcelable.get(key));
+                }
+            }
+        }
+
+        if (!unexpectedAdditionalObjects.isEmpty()) {
+            Iterator<String> iterator = unexpectedAdditionalObjects.keySet().iterator();
+            while (iterator.hasNext()) {
+                String key = iterator.next();
+                Object value = unexpectedAdditionalObjects.get(key);
+                if (value instanceof String) {
+                    description.appendText("\n unexpected String found in actual Bundle: ").appendValue(key).appendText(", ").appendValue(value);
+                } else if (value instanceof Boolean) {
+                    description.appendText("\n unexpected Boolean found in actual Bundle: ").appendValue(key).appendText(", ").appendValue(value);
+                } else if (value instanceof Integer) {
+                    description.appendText("\n unexpected Integer found in actual Bundle: ").appendValue(key).appendText(", ").appendValue(value);
+                } else if (value instanceof Parcelable) {
+                    description.appendText("\n unexpected Parcelable found in actual Bundle: ").appendValue(key).appendText(", ").appendValue(value);
+                } else {
+                    description.appendText("\n unexpected Object found in actual Bundle: ").appendValue(key).appendText(", ").appendValue(value);
+                }
+            }
+        }
+    }
+
+    private boolean checkActualBundleHasAllExpectedBooleanValues(Bundle actualBundle) {
+        Set<String> booleanKeys = expectedBooleans.keySet();
+        for (String key : booleanKeys) {
+            Object valueObject = actualBundle.get(key);
+            if (valueObject == null ||
+                    !(valueObject instanceof Boolean) ||
+                    valueObject != expectedBooleans.get(key)) {
+                unfoundExpectedBoolean.put(key, (Boolean) valueObject);
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private boolean checkActualBundleHasAllExpectedStringValues(Bundle actualBundle) {
+        Set<String> stringKeys = expectedStrings.keySet();
+        for (String key : stringKeys) {
+            Object valueObject = actualBundle.get(key);
+            if (valueObject == null ||
+                    !(valueObject instanceof String) ||
+                    !valueObject.equals(expectedStrings.get(key))) {
+                unfoundExpectedString.put(key, (String) valueObject);
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private boolean checkActualBundleHasAllExpectedIntValues(Bundle actualBundle) {
+        Set<String> stringKeys = expectedIntegers.keySet();
+        for (String key : stringKeys) {
+            Object valueObject = actualBundle.get(key);
+            if (valueObject == null ||
+                    !(valueObject instanceof Integer) ||
+                    ((Integer) valueObject).compareTo(expectedIntegers.get(key)) != 0) {
+                unfoundExpectedInteger.put(key, (Integer) valueObject);
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private boolean checkActualBundleHasAllExpectedParcelableValues(Bundle actualBundle) {
+        Set<String> stringKeys = expectedParcelables.keySet();
+        for (String key : stringKeys) {
+            Object valueObject = actualBundle.get(key);
+            if (valueObject == null ||
+                    !(valueObject instanceof Parcelable) ||
+                    !valueObject.equals(expectedParcelables.get(key))) {
+                unfoundExpectedParcelable.put(key, (Parcelable) valueObject);
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private boolean checkUnexpectedAdditionalValuesIn(Bundle actualBundle) {
+        Set<String> keys = actualBundle.keySet();
+
+        for (String key : keys) {
+            if (!expectedStrings.containsKey(key) &&
+                    !expectedIntegers.containsKey(key) &&
+                    !expectedBooleans.containsKey(key) &&
+                    !expectedParcelables.containsKey(key)
+                    ) {
+                unexpectedAdditionalObjects.put(key, actualBundle.getString(key));
+            }
+        }
+        return unexpectedAdditionalObjects.isEmpty();
+    }
+}
diff --git a/app/src/test/resources/error_messages.json b/app/src/test/resources/error_messages.json
new file mode 100644
index 0000000000000000000000000000000000000000..4d72b074e4ce6bcafcb91a7638db005147d7fc23
--- /dev/null
+++ b/app/src/test/resources/error_messages.json
@@ -0,0 +1,16 @@
+{
+  "server_unreachable_message": "Server is unreachable, please try again.",
+"error.security.pinnedcertificate": "Security error, update the app or choose another provider.",
+"malformed_url": "It doesn't seem to be a Bitmask provider.",
+"certificate_error": "This is not a trusted Bitmask provider.",
+"error_srp_math_error_user_message": "Try again: server math error.",
+"error_bad_user_password_user_message": "Incorrect username or password.",
+"error_not_valid_password_user_message": "It should have at least 8 characters.",
+"error_client_http_user_message": "Try again: Client HTTP error",
+"error_io_exception_user_message": "Try again: I/O error",
+"error_json_exception_user_message": "Try again: Bad response from the server",
+"error_no_such_algorithm_exception_user_message": "Encryption algorithm not found. Please update your OS!",
+"warning_corrupted_provider_details": "Stored provider details are corrupted. You can either update Bitmask (recommended) or update the provider details using a commercial CA certificate.",
+"warning_corrupted_provider_cert": "Stored provider certificate is invalid. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate.",
+"warning_expired_provider_cert": "Stored provider certificate is expired. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate."
+}
\ No newline at end of file
diff --git a/app/src/test/resources/outdated_cert.pem b/app/src/test/resources/outdated_cert.pem
new file mode 100644
index 0000000000000000000000000000000000000000..269efe5f6cbd5de06bf0e80e19a1c2dfee0231fd
--- /dev/null
+++ b/app/src/test/resources/outdated_cert.pem
@@ -0,0 +1,33 @@
+-----BEGIN CERTIFICATE-----
+MIIFyTCCA7GgAwIBAgIJANNtrHEcx/tBMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNV
+BAYTAlVTMQswCQYDVQQIDAJOWTEWMBQGA1UEBwwNTmV3IFlvcmsgQ2l0eTENMAsG
+A1UECgwETEVBUDEVMBMGA1UECwwMVGVzdGluZyBEZXAuMSEwHwYJKoZIhvcNAQkB
+FhJkb25vdHJlcGx5QGxlYXAuc2UwHhcNMTgwMTA5MjMxNjA2WhcNMTgwMTA4MjMx
+NjA2WjB7MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTlkxFjAUBgNVBAcMDU5ldyBZ
+b3JrIENpdHkxDTALBgNVBAoMBExFQVAxFTATBgNVBAsMDFRlc3RpbmcgRGVwLjEh
+MB8GCSqGSIb3DQEJARYSZG9ub3RyZXBseUBsZWFwLnNlMIICIjANBgkqhkiG9w0B
+AQEFAAOCAg8AMIICCgKCAgEA2RE9GgRSt9re96fKpBjZ2sKv+YC+efULvp1+3/RB
+2SQP1vcSsNWDtiPq+KpyMlmfou6Xuvuz8c5YEbWyjFHC/IimMu8GG2XAqfs1Zmrj
+IKFX/7Zwprf5OYpfe5BaDV2bKXS+/nHk1GPeZNWWlKZfI/f2mE3p4phwCPjKxFmo
+A4WDq5u1rxQ+iskTi3PEKiO5S7lE7/MuPuWuYDLLyia2VkZddS7/OhxhhtBI8U7k
+VUjY8VeyHqa1w9wxzZOovUXFmrsBbzg0D0BXrafZv6heVZZZFC9DRp3OXBJLbZOM
+gYhK7WIZTRfzl1km+U3Iw+ZUr/bXYy0HRRXq5h3mcXhHnVSBu9uUJYgTaSzWNCpL
+VzbEjYmFlsVsEcFmOTBwKEDmlVwwPuzhreFDkxXHX28xrw6laoClcngYG93pFWw7
+e1NrUuqTw4eR4uM1ZEF7gBwYFKu8B/51Z5wGaYKsbdjOYcEGNqSNBa/emlVByUkG
+kZ9RIiorUoiagCvQBCZCTgwTQ5RQFRx+I5eMjTq+bcbMkdl6/MFfZJg1c+ZVHNpW
+2Q+asu6JZG9MGa0QtJjLSQCE5XFxrG/pQ/2x4PEbS131WUl/BIYLUVsB8ElFerGY
+4D8aAo/z5kPlwtX05lZ9AfchD+iunjEFMaJNib2QevBzk6xOGacuLkB0yu4ep9gU
+qFkCAwEAAaNQME4wHQYDVR0OBBYEFMTuQ+qLzmPNXVMCW2a68cLfKYwqMB8GA1Ud
+IwQYMBaAFMTuQ+qLzmPNXVMCW2a68cLfKYwqMAwGA1UdEwQFMAMBAf8wDQYJKoZI
+hvcNAQELBQADggIBAEfqSITTr7H86ZCrOt2AtDEucN58qyKd9RLEBdxkaPB1yUyu
+iHDiRoE0gvMVu6HUXGLsj3kaAjWdsS+wSfXBIXFZiKeeW6qnBwRNk8dA0vYrS4Ag
+YRS3BgCADlL+NKBdwoKYCNvgIV68XwjcvfEMEkoULr1WUlikX5MH33XKNtjVIid0
+wqswY2wZrRrwssNUz7tXBuBczj7FNJyboDruYyTDloIXoqiqRw1eexA183HeMIad
+leQwVBBdp1drhZdBUwI3r+MtZuyJTkf0+AFAqjKTptu5XvExYj9wvHYsohW5PV8L
+RgzQ6oXCd6s7grvVQEkOXiMq5m3bS4Y1wHd1ispoMUSeYYHhMcNJuanjpulEuWmu
+k4aEuGsgTAtDXNO7nBRw82cbJySoOWT0uKjzU0nT80VkuM45eFD71J1rZIoqJRoP
+jXVzei1B6jcyh2nfKRAkYPd8V9fu50nlUOfJeGAiWxTyBNEhmWiEc4MeDrw3tiIj
+zUrLveNr0z00rCKvuTBPVjM+FQ8Pg0FNAxVJUeaz4qpG3TT7QI+Np7wMqtJYNA3A
+R3JjaZXGx6dF/0+2fUZHQKJvvWNAp8Xu9DouRQWGR1pmAbWpB8xL7zC6S8wWySIH
+07nOyVLK2gFm5jvstvaHQvGPK6Xb4ydlp550vy8NQ9ji9cWehdJdzQ9bjKL6
+-----END CERTIFICATE-----
diff --git a/app/src/test/resources/riseup.net.json b/app/src/test/resources/riseup.net.json
new file mode 100644
index 0000000000000000000000000000000000000000..9a5ec79e24d8ed16451262dab304a94fa97a6e89
--- /dev/null
+++ b/app/src/test/resources/riseup.net.json
@@ -0,0 +1,37 @@
+{
+  "api_uri": "https://api.black.riseup.net:443",
+  "api_version": "1",
+  "ca_cert_fingerprint": "SHA256: a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494",
+  "ca_cert_uri": "https://black.riseup.net/ca.crt",
+  "default_language": "en",
+  "description": {
+    "en": "Riseup is a non-profit collective in Seattle that provides online communication tools for people and groups working toward liberatory social change."
+  },
+  "domain": "riseup.net",
+  "enrollment_policy": "open",
+  "languages": [
+    "en"
+  ],
+  "name": {
+    "en": "Riseup Networks"
+  },
+  "service": {
+    "allow_anonymous": false,
+    "allow_free": true,
+    "allow_limited_bandwidth": false,
+    "allow_paid": false,
+    "allow_registration": true,
+    "allow_unlimited_bandwidth": true,
+    "bandwidth_limit": 102400,
+    "default_service_level": 1,
+    "levels": {
+      "1": {
+        "description": "Please donate.",
+        "name": "free"
+      }
+    }
+  },
+  "services": [
+    "openvpn"
+  ]
+}
\ No newline at end of file
diff --git a/app/src/test/resources/riseup.net.pem b/app/src/test/resources/riseup.net.pem
new file mode 100644
index 0000000000000000000000000000000000000000..c890aff40d14cfaf7f0dc475322e5592c9274f0a
--- /dev/null
+++ b/app/src/test/resources/riseup.net.pem
@@ -0,0 +1,32 @@
+-----BEGIN CERTIFICATE-----
+MIIFjTCCA3WgAwIBAgIBATANBgkqhkiG9w0BAQ0FADBZMRgwFgYDVQQKDA9SaXNl
+dXAgTmV0d29ya3MxGzAZBgNVBAsMEmh0dHBzOi8vcmlzZXVwLm5ldDEgMB4GA1UE
+AwwXUmlzZXVwIE5ldHdvcmtzIFJvb3QgQ0EwHhcNMTQwNDI4MDAwMDAwWhcNMjQw
+NDI4MDAwMDAwWjBZMRgwFgYDVQQKDA9SaXNldXAgTmV0d29ya3MxGzAZBgNVBAsM
+Emh0dHBzOi8vcmlzZXVwLm5ldDEgMB4GA1UEAwwXUmlzZXVwIE5ldHdvcmtzIFJv
+b3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC76J4ciMJ8Sg0m
+TP7DF2DT9zNe0Csk4myoMFC57rfJeqsAlJCv1XMzBmXrw8wq/9z7XHv6n/0sWU7a
+7cF2hLR33ktjwODlx7vorU39/lXLndo492ZBhXQtG1INMShyv+nlmzO6GT7ESfNE
+LliFitEzwIegpMqxCIHXFuobGSCWF4N0qLHkq/SYUMoOJ96O3hmPSl1kFDRMtWXY
+iw1SEKjUvpyDJpVs3NGxeLCaA7bAWhDY5s5Yb2fA1o8ICAqhowurowJpW7n5ZuLK
+5VNTlNy6nZpkjt1QycYvNycffyPOFm/Q/RKDlvnorJIrihPkyniV3YY5cGgP+Qkx
+HUOT0uLA6LHtzfiyaOqkXwc4b0ZcQD5Vbf6Prd20Ppt6ei0zazkUPwxld3hgyw58
+m/4UIjG3PInWTNf293GngK2Bnz8Qx9e/6TueMSAn/3JBLem56E0WtmbLVjvko+LF
+PM5xA+m0BmuSJtrD1MUCXMhqYTtiOvgLBlUm5zkNxALzG+cXB28k6XikXt6MRG7q
+hzIPG38zwkooM55yy5i1YfcIi5NjMH6A+t4IJxxwb67MSb6UFOwg5kFokdONZcwj
+shczHdG9gLKSBIvrKa03Nd3W2dF9hMbRu//STcQxOailDBQCnXXfAATj9pYzdY4k
+ha8VCAREGAKTDAex9oXf1yRuktES4QIDAQABo2AwXjAdBgNVHQ4EFgQUC4tdmLVu
+f9hwfK4AGliaet5KkcgwDgYDVR0PAQH/BAQDAgIEMAwGA1UdEwQFMAMBAf8wHwYD
+VR0jBBgwFoAUC4tdmLVuf9hwfK4AGliaet5KkcgwDQYJKoZIhvcNAQENBQADggIB
+AGzL+GRnYu99zFoy0bXJKOGCF5XUXP/3gIXPRDqQf5g7Cu/jYMID9dB3No4Zmf7v
+qHjiSXiS8jx1j/6/Luk6PpFbT7QYm4QLs1f4BlfZOti2KE8r7KRDPIecUsUXW6P/
+3GJAVYH/+7OjA39za9AieM7+H5BELGccGrM5wfl7JeEz8in+V2ZWDzHQO4hMkiTQ
+4ZckuaL201F68YpiItBNnJ9N5nHr1MRiGyApHmLXY/wvlrOpclh95qn+lG6/2jk7
+3AmihLOKYMlPwPakJg4PYczm3icFLgTpjV5sq2md9bRyAg3oPGfAuWHmKj2Ikqch
+Td5CHKGxEEWbGUWEMP0s1A/JHWiCbDigc4Cfxhy56CWG4q0tYtnc2GMw8OAUO6Wf
+Xu5pYKNkzKSEtT/MrNJt44tTZWbKV/Pi/N2Fx36my7TgTUj7g3xcE9eF4JV2H/sg
+tsK3pwE0FEqGnT4qMFbixQmc8bGyuakr23wjMvfO7eZUxBuWYR2SkcP26sozF9PF
+tGhbZHQVGZUTVPyvwahMUEhbPGVerOW0IYpxkm0x/eaWdTc4vPpf/rIlgbAjarnJ
+UN9SaWRlWKSdP4haujnzCoJbM7dU9bjvlGZNyXEekgeT0W2qFeGGp+yyUWw8tNsp
+0BuC1b7uW/bBn/xKm319wXVDvBgZgcktMolak39V7DVO
+-----END CERTIFICATE-----
\ No newline at end of file
diff --git a/app/src/test/resources/riseup.service.json b/app/src/test/resources/riseup.service.json
new file mode 100644
index 0000000000000000000000000000000000000000..05f23bc19d5e5073e51b4cf44065a66a05f7909b
--- /dev/null
+++ b/app/src/test/resources/riseup.service.json
@@ -0,0 +1,93 @@
+{
+  "gateways":[
+    {
+      "capabilities":{
+        "adblock":false,
+        "filter_dns":false,
+        "limited":false,
+        "ports":[
+          "443"
+        ],
+        "protocols":[
+          "tcp"
+        ],
+        "transport":[
+          "openvpn"
+        ],
+        "user_ips":false
+      },
+      "host":"garza.riseup.net",
+      "ip_address":"198.252.153.28",
+      "location":"seattle"
+    },
+    {
+      "capabilities":{
+        "adblock":false,
+        "filter_dns":false,
+        "limited":false,
+        "ports":[
+          "443"
+        ],
+        "protocols":[
+          "tcp"
+        ],
+        "transport":[
+          "openvpn"
+        ],
+        "user_ips":false
+      },
+      "host":"tenca.riseup.net",
+      "ip_address":"5.79.86.180",
+      "location":"amsterdam"
+    },
+    {
+      "capabilities":{
+        "adblock":false,
+        "filter_dns":false,
+        "limited":false,
+        "ports":[
+          "443"
+        ],
+        "protocols":[
+          "tcp"
+        ],
+        "transport":[
+          "openvpn"
+        ],
+        "user_ips":false
+      },
+      "host":"yal.riseup.net",
+      "ip_address":"199.58.81.145",
+      "location":"montreal"
+    }
+  ],
+  "locations":{
+    "amsterdam":{
+      "country_code":"NL",
+      "hemisphere":"N",
+      "name":"Amsterdam",
+      "timezone":"+2"
+    },
+    "montreal":{
+      "country_code":"CA",
+      "hemisphere":"N",
+      "name":"Montreal",
+      "timezone":"-5"
+    },
+    "seattle":{
+      "country_code":"US",
+      "hemisphere":"N",
+      "name":"Seattle",
+      "timezone":"-7"
+    }
+  },
+  "openvpn_configuration":{
+    "auth":"SHA1",
+    "cipher":"AES-128-CBC",
+    "keepalive":"10 30",
+    "tls-cipher":"DHE-RSA-AES128-SHA",
+    "tun-ipv6":true
+  },
+  "serial":1,
+  "version":1
+}
\ No newline at end of file
diff --git a/app/src/test/resources/secrets.json b/app/src/test/resources/secrets.json
index 36ba5977a3fdb01a9017ec32e8137dc89e003b80..f3b689196765d5d1bcf8d255a8b052addca1d62e 100644
--- a/app/src/test/resources/secrets.json
+++ b/app/src/test/resources/secrets.json
@@ -1 +1 @@
-{"ca_cert":"-----BEGIN CERTIFICATE-----\nMIIFbzCCA1egAwIBAgIBATANBgkqhkiG9w0BAQ0FADBKMRgwFgYDVQQDDA9CaXRt\nYXNrIFJvb3QgQ0ExEDAOBgNVBAoMB0JpdG1hc2sxHDAaBgNVBAsME2h0dHBzOi8v\nYml0bWFzay5uZXQwHhcNMTIxMTA2MDAwMDAwWhcNMjIxMTA2MDAwMDAwWjBKMRgw\nFgYDVQQDDA9CaXRtYXNrIFJvb3QgQ0ExEDAOBgNVBAoMB0JpdG1hc2sxHDAaBgNV\nBAsME2h0dHBzOi8vYml0bWFzay5uZXQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw\nggIKAoICAQC1eV4YvayaU+maJbWrD4OHo3d7S1BtDlcvkIRS1Fw3iYDjsyDkZxai\ndHp4EUasfNQ+EVtXUvtk6170EmLco6Elg8SJBQ27trE6nielPRPCfX3fQzETRfvB\n7tNvGw4Jn2YKiYoMD79kkjgyZjkJ2r\/bEHUSevmR09BRp86syHZerdNGpXYhcQ84\nCA1+V+603GFIHnrP+uQDdssW93rgDNYu+exT+Wj6STfnUkugyjmPRPjL7wh0tzy+\nznCeLl4xiV3g9sjPnc7r2EQKd5uaTe3j71sDPF92KRk0SSUndREz+B1+Dbe\/RGk4\nMEqGFuOzrtsgEhPIX0hplhb0Tgz\/rtug+yTT7oJjBa3u20AAOQ38\/M99EfdeJvc4\nlPFF1XBBLh6X9UKF72an2NuANiX6XPySnJgZ7nZ09RiYZqVwu\/qt3DfvLfhboq+0\nbQvLUPXrVDr70onv5UDjpmEA\/cLmaIqqrduuTkFZOym65\/PfAPvpGnt7crQj\/Ibl\nDEDYZQmP7AS+6zBjoOzNjUGE5r40zWAR1RSi7zliXTu+yfsjXUIhUAWmYR6J3KxB\nlfsiHBQ+8dn9kC3YrUexWoOqBiqJOAJzZh5Y1tqgzfh+2nmHSB2dsQRs7rDRRlyy\nYMbkpzL9ZsOUO2eTP1mmar6YjCN+rggYjRrX71K2SpBG6b1zZxOG+wIDAQABo2Aw\nXjAdBgNVHQ4EFgQUuYGDLL2sswnYpHHvProt1JU+D48wDgYDVR0PAQH\/BAQDAgIE\nMAwGA1UdEwQFMAMBAf8wHwYDVR0jBBgwFoAUuYGDLL2sswnYpHHvProt1JU+D48w\nDQYJKoZIhvcNAQENBQADggIBADeG67vaFcbITGpi51264kHPYPEWaXUa5XYbtmBl\ncXYyB6hY5hv\/YNuVGJ1gWsDmdeXEyj0j2icGQjYdHRfwhrbEri+h1EZOm1cSBDuY\nk\/P5+ctHyOXx8IE79DBsZ6IL61UKIaKhqZBfLGYcWu17DVV6+LT+AKtHhOrv3TSj\nRnAcKnCbKqXLhUPXpK0eTjPYS2zQGQGIhIy9sQXVXJJJsGrPgMxna1Xw2JikBOCG\nhtD\/JKwt6xBmNwktH0GI\/LVtVgSp82Clbn9C4eZN9E5YbVYjLkIEDhpByeC71QhX\nEIQ0ZR56bFuJA\/CwValBqV\/G9gscTPQqd+iETp8yrFpAVHOW+YzSFbxjTEkBte1J\naF0vmbqdMAWLk+LEFPQRptZh0B88igtx6tV5oVd+p5IVRM49poLhuPNJGPvMj99l\nmlZ4+AeRUnbOOeAEuvpLJbel4rhwFzmUiGoeTVoPZyMevWcVFq6BMkS+jRR2w0jK\nG6b0v5XDHlcFYPOgUrtsOBFJVwbutLvxdk6q37kIFnWCd8L3kmES5q4wjyFK47Co\nJa8zlx64jmMZPg\/t3wWqkZgXZ14qnbyG5\/lGsj5CwVtfDljrhN0oCWK1FZaUmW3d\n69db12\/g4f6phldhxiWuGC\/W6fCW5kre7nmhshcltqAJJuU47iX+DarBFiIj816e\nyV8e\n-----END CERTIFICATE-----\n","cert":"-----BEGIN CERTIFICATE-----\nMIIEjDCCAnSgAwIBAgIQG6MBp\/cd9DlY+7cdvp3R3jANBgkqhkiG9w0BAQsFADBmMRAwDgYDVQQK\nDAdCaXRtYXNrMRwwGgYDVQQLDBNodHRwczovL2JpdG1hc2submV0MTQwMgYDVQQDDCtCaXRtYXNr\nIFJvb3QgQ0EgKGNsaWVudCBjZXJ0aWZpY2F0ZXMgb25seSEpMB4XDTE0MTIwNTAwMDAwMFoXDTE1\nMDMwNTAwMDAwMFowLTErMCkGA1UEAwwiVU5MSU1JVEVEZDBwZDdkMzE4eTNtOHNkeXllaTFqYmZl\neDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANRNhZ4aCwdL5+OKObOKeI2rDqEwGnIr\nhL9wzo\/FXbwLfdW45Y9Mxwhh6xy2NkA1YUKCB8VNBKNXlBrGr1QriLbu1rItsJ2VVLqGluVV\/gO4\njcaPU+\/Wu0hMFKG28J\/dPvIGeNbjBWk6mxQAA5WIpRK9RTeQ88wVaGIZDDzIdivza2zpcyiPAyii\ndbkyXh7sLsKvbZB6wLrert6Y1ylR3SlkZP0LfdGAMAdkMyuXKOjgcSnUltR8HSBuZcSUlsTVM11n\nrYeGCYyPNNQ3UYatDW33UASgRDBorrmjhhKP7IW\/opdlnPk5ZrP3i0qI32\/boRe0EWZGXJvr4P3K\ndJ30uCECAwEAAaNvMG0wHQYDVR0OBBYEFK8bMVAM4GBB5sHptoIOAaIvlYueMAsGA1UdDwQEAwIH\ngDATBgNVHSUEDDAKBggrBgEFBQcDAjAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFId+E7bsWFsUWah9\nvZuPvZ7O+aJsMA0GCSqGSIb3DQEBCwUAA4ICAQAQOX81csVhvP422NKkZH7+g3npBpl+sEHedaGR\nxYPOu4HrA4TVF9h44sljRoRJyenGNdBZCXcLKHg889eePTf8Z5K3lTojp6hvwyA6tgxOMHT1kESW\nPfqnRw8mHfHJuE3g+4YNUMwggzwc\/VZATdV\/7M33sarVN9AUOHou9n9BizgCC+UnYlS+F2POumE3\nFbOhKo5uubI02MwBYlN2JVO2TBt1Q20w8wc6cU07Xi5Epp+1mkgFiOShkNtPcJmEyBWJhxDtSDOW\n2doqWYNqH2kq7B5R\/kyyfcpFJqAnBTV7xs+C5rTS1mW7LpxfdCUMbYuLCpyxpO3A\/DhAm8n47tUH\nlBtmo8Avdb8VdFpYiGBpB0o9kTFcsWFb2GkWFBduGfSEB8jUI7QtqhgZqocAKK\/cweSRV8FwyUcn\nR0prRm3QEi9fbXqEddzjSY9y\/lqWYzT7u+IOAQpKroeZ4wzgYperDNOUFuYk1rP7yuvjP2pV5rcN\nyPoBP60TPVWMRM4WJm6nTogAz2qBrFsf\/XwT\/ajzbsjT6HNB7QbRE+wkFkqspoXG5Agp7KQ8lW3L\nSKCDGOQJz7VIE85pD0tg7QEXBEw8oaRZtMjQ0Gvs25mxXAKka4wGasaWfYH6d0E+iKYcWn86V1rH\nK2ZoknT+Nno5jgjFuUR3fZseNizEfx7BteooKQ==\n-----END CERTIFICATE-----","Constants.PRIVATE_KEY":"-----BEGIN RSA PRIVATE KEY-----\nMIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDUTYWeGgsHS+fjijmziniNqw6h\nMBpyK4S\/cM6PxV28C33VuOWPTMcIYesctjZANWFCggfFTQSjV5Qaxq9UK4i27tayLbCdlVS6hpbl\nVf4DuI3Gj1Pv1rtITBShtvCf3T7yBnjW4wVpOpsUAAOViKUSvUU3kPPMFWhiGQw8yHYr82ts6XMo\njwMoonW5Ml4e7C7Cr22QesC63q7emNcpUd0pZGT9C33RgDAHZDMrlyjo4HEp1JbUfB0gbmXElJbE\n1TNdZ62HhgmMjzTUN1GGrQ1t91AEoEQwaK65o4YSj+yFv6KXZZz5OWaz94tKiN9v26EXtBFmRlyb\n6+D9ynSd9LghAgMBAAECggEBANPHLRXkhsHVj1EkzqBx7gXr8CEMmiTvknFh9zvltrZhhDoRQjWr\nchPDkcRHY2Cznvy4N0YyqQDD2ULIlZdSAgPxxothFoBruWSD47yMBmLx08ORsDpcqt\/YvPAATJI8\nIpFNsXcyaXBp\/M57oRemgnxp\/8UJPJmFdWX99H4hvffh\/jdj7POgYiWUaAl37XTYZKZ4nzKU2wpL\nEDLj9RKPz9gG7CYp2zrLC9LaAsrXVrKwPBw6g+XwbClaqFj97db3mrY4lr6mTo89qmus1AU+fBDH\n3Xlpmc8JwB+30TvhRNKrpLx9cEjuEj7K1gm8Y4dWCjPi+lNbtAyUBcgPJFa\/81ECgYEA7pLoBU\/Y\nZYjyHFca8FvDBcBh6haHfqJr9doXWtgjDrbi3o2n5wHqfKhFWOH6vPEQozkOVeX1ze6HOiRmGBpW\nr+r7x8TD25L7I6HJw3M351RWOAfkF0w\/RTVdetcTgduQtfN1u6BDhYSVceXMjyQYx7MhfETWI8Gh\nKSYm8OEDYiUCgYEA489fmbrCcUnXzpTsbswJ5NmSoEXbcX8cLxnQuzE0z9GHhQdrMjOpXR76reTW\n6jcuudarNcwRUYSWWhjCDKHhpx4HhasWPaHgr7jIzcRw8yZSJRSxKr8sl1qh6g7s47JcmfXOMWLt\nyuyE933XrT19Th4ODZHY40Uv35mPjMi9d00CgYEAyRNAQtndBRa7GG\/B4Ls2T+6pl+aNJIo4e+no\nrURlp800wWabEPRocdBRQmyULBLxduBr2LIMzhgwGSz8b2wji\/l9ZA3PFY135bxClVzSzUIjuO3N\nrGUzHl2wAAyuAFDSUshzfkPBJRNt8aVBF5PQ3t93ZYmPAmv8LPZe875yX5ECgYEAsUEcwK\/ZNW7g\ndQPZR4iJNkC4Xu6cBZ6Cnn92swBheEYvLSoNlX0vDZ7aLE3\/jzQqrjzC8NP8sbH5jtbuvgeDXZX3\nAmGRp5j6C6A61ihAPmEVz3ZfN8SSfJ3vl\/\/PAIg6lyz0J+cy4Q7RkwSeuVQ72Hl4M8TEvmmKC3Af\nispy6Y0CgYEAgl1o2lo+ACyk+oVQPaaPqK3d7WOBFp4eR2nXFor\/vsx9igQOlZUgzRDQsR8jo1o9\nefOSBf87igrZGgssys89pWa2dnXnz5PMmzkKr6bw4D9Ez6u6Puc9UZhGw\/8wDYg6fSosdB9utspm\nM698ycef7jBNMDgmhpSvfw5GctoNQ4s=\n-----END RSA PRIVATE KEY-----"}
+{"ca_cert":"-----BEGIN CERTIFICATE-----\nMIIFbzCCA1egAwIBAgIBATANBgkqhkiG9w0BAQ0FADBKMRgwFgYDVQQDDA9CaXRt\nYXNrIFJvb3QgQ0ExEDAOBgNVBAoMB0JpdG1hc2sxHDAaBgNVBAsME2h0dHBzOi8v\nYml0bWFzay5uZXQwHhcNMTIxMTA2MDAwMDAwWhcNMjIxMTA2MDAwMDAwWjBKMRgw\nFgYDVQQDDA9CaXRtYXNrIFJvb3QgQ0ExEDAOBgNVBAoMB0JpdG1hc2sxHDAaBgNV\nBAsME2h0dHBzOi8vYml0bWFzay5uZXQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw\nggIKAoICAQC1eV4YvayaU+maJbWrD4OHo3d7S1BtDlcvkIRS1Fw3iYDjsyDkZxai\ndHp4EUasfNQ+EVtXUvtk6170EmLco6Elg8SJBQ27trE6nielPRPCfX3fQzETRfvB\n7tNvGw4Jn2YKiYoMD79kkjgyZjkJ2r\/bEHUSevmR09BRp86syHZerdNGpXYhcQ84\nCA1+V+603GFIHnrP+uQDdssW93rgDNYu+exT+Wj6STfnUkugyjmPRPjL7wh0tzy+\nznCeLl4xiV3g9sjPnc7r2EQKd5uaTe3j71sDPF92KRk0SSUndREz+B1+Dbe\/RGk4\nMEqGFuOzrtsgEhPIX0hplhb0Tgz\/rtug+yTT7oJjBa3u20AAOQ38\/M99EfdeJvc4\nlPFF1XBBLh6X9UKF72an2NuANiX6XPySnJgZ7nZ09RiYZqVwu\/qt3DfvLfhboq+0\nbQvLUPXrVDr70onv5UDjpmEA\/cLmaIqqrduuTkFZOym65\/PfAPvpGnt7crQj\/Ibl\nDEDYZQmP7AS+6zBjoOzNjUGE5r40zWAR1RSi7zliXTu+yfsjXUIhUAWmYR6J3KxB\nlfsiHBQ+8dn9kC3YrUexWoOqBiqJOAJzZh5Y1tqgzfh+2nmHSB2dsQRs7rDRRlyy\nYMbkpzL9ZsOUO2eTP1mmar6YjCN+rggYjRrX71K2SpBG6b1zZxOG+wIDAQABo2Aw\nXjAdBgNVHQ4EFgQUuYGDLL2sswnYpHHvProt1JU+D48wDgYDVR0PAQH\/BAQDAgIE\nMAwGA1UdEwQFMAMBAf8wHwYDVR0jBBgwFoAUuYGDLL2sswnYpHHvProt1JU+D48w\nDQYJKoZIhvcNAQENBQADggIBADeG67vaFcbITGpi51264kHPYPEWaXUa5XYbtmBl\ncXYyB6hY5hv\/YNuVGJ1gWsDmdeXEyj0j2icGQjYdHRfwhrbEri+h1EZOm1cSBDuY\nk\/P5+ctHyOXx8IE79DBsZ6IL61UKIaKhqZBfLGYcWu17DVV6+LT+AKtHhOrv3TSj\nRnAcKnCbKqXLhUPXpK0eTjPYS2zQGQGIhIy9sQXVXJJJsGrPgMxna1Xw2JikBOCG\nhtD\/JKwt6xBmNwktH0GI\/LVtVgSp82Clbn9C4eZN9E5YbVYjLkIEDhpByeC71QhX\nEIQ0ZR56bFuJA\/CwValBqV\/G9gscTPQqd+iETp8yrFpAVHOW+YzSFbxjTEkBte1J\naF0vmbqdMAWLk+LEFPQRptZh0B88igtx6tV5oVd+p5IVRM49poLhuPNJGPvMj99l\nmlZ4+AeRUnbOOeAEuvpLJbel4rhwFzmUiGoeTVoPZyMevWcVFq6BMkS+jRR2w0jK\nG6b0v5XDHlcFYPOgUrtsOBFJVwbutLvxdk6q37kIFnWCd8L3kmES5q4wjyFK47Co\nJa8zlx64jmMZPg\/t3wWqkZgXZ14qnbyG5\/lGsj5CwVtfDljrhN0oCWK1FZaUmW3d\n69db12\/g4f6phldhxiWuGC\/W6fCW5kre7nmhshcltqAJJuU47iX+DarBFiIj816e\nyV8e\n-----END CERTIFICATE-----\n","cert":"-----BEGIN CERTIFICATE-----\nMIIEjDCCAnSgAwIBAgIQG6MBp\/cd9DlY+7cdvp3R3jANBgkqhkiG9w0BAQsFADBmMRAwDgYDVQQK\nDAdCaXRtYXNrMRwwGgYDVQQLDBNodHRwczovL2JpdG1hc2submV0MTQwMgYDVQQDDCtCaXRtYXNr\nIFJvb3QgQ0EgKGNsaWVudCBjZXJ0aWZpY2F0ZXMgb25seSEpMB4XDTE0MTIwNTAwMDAwMFoXDTE1\nMDMwNTAwMDAwMFowLTErMCkGA1UEAwwiVU5MSU1JVEVEZDBwZDdkMzE4eTNtOHNkeXllaTFqYmZl\neDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANRNhZ4aCwdL5+OKObOKeI2rDqEwGnIr\nhL9wzo\/FXbwLfdW45Y9Mxwhh6xy2NkA1YUKCB8VNBKNXlBrGr1QriLbu1rItsJ2VVLqGluVV\/gO4\njcaPU+\/Wu0hMFKG28J\/dPvIGeNbjBWk6mxQAA5WIpRK9RTeQ88wVaGIZDDzIdivza2zpcyiPAyii\ndbkyXh7sLsKvbZB6wLrert6Y1ylR3SlkZP0LfdGAMAdkMyuXKOjgcSnUltR8HSBuZcSUlsTVM11n\nrYeGCYyPNNQ3UYatDW33UASgRDBorrmjhhKP7IW\/opdlnPk5ZrP3i0qI32\/boRe0EWZGXJvr4P3K\ndJ30uCECAwEAAaNvMG0wHQYDVR0OBBYEFK8bMVAM4GBB5sHptoIOAaIvlYueMAsGA1UdDwQEAwIH\ngDATBgNVHSUEDDAKBggrBgEFBQcDAjAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFId+E7bsWFsUWah9\nvZuPvZ7O+aJsMA0GCSqGSIb3DQEBCwUAA4ICAQAQOX81csVhvP422NKkZH7+g3npBpl+sEHedaGR\nxYPOu4HrA4TVF9h44sljRoRJyenGNdBZCXcLKHg889eePTf8Z5K3lTojp6hvwyA6tgxOMHT1kESW\nPfqnRw8mHfHJuE3g+4YNUMwggzwc\/VZATdV\/7M33sarVN9AUOHou9n9BizgCC+UnYlS+F2POumE3\nFbOhKo5uubI02MwBYlN2JVO2TBt1Q20w8wc6cU07Xi5Epp+1mkgFiOShkNtPcJmEyBWJhxDtSDOW\n2doqWYNqH2kq7B5R\/kyyfcpFJqAnBTV7xs+C5rTS1mW7LpxfdCUMbYuLCpyxpO3A\/DhAm8n47tUH\nlBtmo8Avdb8VdFpYiGBpB0o9kTFcsWFb2GkWFBduGfSEB8jUI7QtqhgZqocAKK\/cweSRV8FwyUcn\nR0prRm3QEi9fbXqEddzjSY9y\/lqWYzT7u+IOAQpKroeZ4wzgYperDNOUFuYk1rP7yuvjP2pV5rcN\nyPoBP60TPVWMRM4WJm6nTogAz2qBrFsf\/XwT\/ajzbsjT6HNB7QbRE+wkFkqspoXG5Agp7KQ8lW3L\nSKCDGOQJz7VIE85pD0tg7QEXBEw8oaRZtMjQ0Gvs25mxXAKka4wGasaWfYH6d0E+iKYcWn86V1rH\nK2ZoknT+Nno5jgjFuUR3fZseNizEfx7BteooKQ==\n-----END CERTIFICATE-----","Constants.PROVIDER_PRIVATE_KEY":"-----BEGIN RSA PRIVATE KEY-----\nMIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDUTYWeGgsHS+fjijmziniNqw6h\nMBpyK4S\/cM6PxV28C33VuOWPTMcIYesctjZANWFCggfFTQSjV5Qaxq9UK4i27tayLbCdlVS6hpbl\nVf4DuI3Gj1Pv1rtITBShtvCf3T7yBnjW4wVpOpsUAAOViKUSvUU3kPPMFWhiGQw8yHYr82ts6XMo\njwMoonW5Ml4e7C7Cr22QesC63q7emNcpUd0pZGT9C33RgDAHZDMrlyjo4HEp1JbUfB0gbmXElJbE\n1TNdZ62HhgmMjzTUN1GGrQ1t91AEoEQwaK65o4YSj+yFv6KXZZz5OWaz94tKiN9v26EXtBFmRlyb\n6+D9ynSd9LghAgMBAAECggEBANPHLRXkhsHVj1EkzqBx7gXr8CEMmiTvknFh9zvltrZhhDoRQjWr\nchPDkcRHY2Cznvy4N0YyqQDD2ULIlZdSAgPxxothFoBruWSD47yMBmLx08ORsDpcqt\/YvPAATJI8\nIpFNsXcyaXBp\/M57oRemgnxp\/8UJPJmFdWX99H4hvffh\/jdj7POgYiWUaAl37XTYZKZ4nzKU2wpL\nEDLj9RKPz9gG7CYp2zrLC9LaAsrXVrKwPBw6g+XwbClaqFj97db3mrY4lr6mTo89qmus1AU+fBDH\n3Xlpmc8JwB+30TvhRNKrpLx9cEjuEj7K1gm8Y4dWCjPi+lNbtAyUBcgPJFa\/81ECgYEA7pLoBU\/Y\nZYjyHFca8FvDBcBh6haHfqJr9doXWtgjDrbi3o2n5wHqfKhFWOH6vPEQozkOVeX1ze6HOiRmGBpW\nr+r7x8TD25L7I6HJw3M351RWOAfkF0w\/RTVdetcTgduQtfN1u6BDhYSVceXMjyQYx7MhfETWI8Gh\nKSYm8OEDYiUCgYEA489fmbrCcUnXzpTsbswJ5NmSoEXbcX8cLxnQuzE0z9GHhQdrMjOpXR76reTW\n6jcuudarNcwRUYSWWhjCDKHhpx4HhasWPaHgr7jIzcRw8yZSJRSxKr8sl1qh6g7s47JcmfXOMWLt\nyuyE933XrT19Th4ODZHY40Uv35mPjMi9d00CgYEAyRNAQtndBRa7GG\/B4Ls2T+6pl+aNJIo4e+no\nrURlp800wWabEPRocdBRQmyULBLxduBr2LIMzhgwGSz8b2wji\/l9ZA3PFY135bxClVzSzUIjuO3N\nrGUzHl2wAAyuAFDSUshzfkPBJRNt8aVBF5PQ3t93ZYmPAmv8LPZe875yX5ECgYEAsUEcwK\/ZNW7g\ndQPZR4iJNkC4Xu6cBZ6Cnn92swBheEYvLSoNlX0vDZ7aLE3\/jzQqrjzC8NP8sbH5jtbuvgeDXZX3\nAmGRp5j6C6A61ihAPmEVz3ZfN8SSfJ3vl\/\/PAIg6lyz0J+cy4Q7RkwSeuVQ72Hl4M8TEvmmKC3Af\nispy6Y0CgYEAgl1o2lo+ACyk+oVQPaaPqK3d7WOBFp4eR2nXFor\/vsx9igQOlZUgzRDQsR8jo1o9\nefOSBf87igrZGgssys89pWa2dnXnz5PMmzkKr6bw4D9Ez6u6Puc9UZhGw\/8wDYg6fSosdB9utspm\nM698ycef7jBNMDgmhpSvfw5GctoNQ4s=\n-----END RSA PRIVATE KEY-----"}
diff --git a/app/src/test/resources/updated_cert.pem b/app/src/test/resources/updated_cert.pem
new file mode 100644
index 0000000000000000000000000000000000000000..21f9a693e9a070fbaa0085b1778a2adf88109987
--- /dev/null
+++ b/app/src/test/resources/updated_cert.pem
@@ -0,0 +1,33 @@
+-----BEGIN CERTIFICATE-----
+MIIFyTCCA7GgAwIBAgIJALD2RMhYzVdWMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNV
+BAYTAlVTMQswCQYDVQQIDAJOWTEWMBQGA1UEBwwNTmV3IFlvcmsgQ2l0eTENMAsG
+A1UECgwETEVBUDEVMBMGA1UECwwMVGVzdGluZyBEZXAuMSEwHwYJKoZIhvcNAQkB
+FhJkb25vdHJlcGx5QGxlYXAuc2UwHhcNMTgwMTA5MjMyMTQzWhcNMzMwMTA1MjMy
+MTQzWjB7MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTlkxFjAUBgNVBAcMDU5ldyBZ
+b3JrIENpdHkxDTALBgNVBAoMBExFQVAxFTATBgNVBAsMDFRlc3RpbmcgRGVwLjEh
+MB8GCSqGSIb3DQEJARYSZG9ub3RyZXBseUBsZWFwLnNlMIICIjANBgkqhkiG9w0B
+AQEFAAOCAg8AMIICCgKCAgEAvJqdroZQdvKOE9o/aga9GRfaY05Bs7EKdRp3pabu
+O5uEyx0tRpbif+rcb3DRDWfIH+wxrNcSp5rRbQollttnH5Sdd0aG7taZLPCMHVlk
+j92RRCggQQd3jrDzzKKNo3080B2BerWRK+Rzr0wVF7iD37Lcz5F74FoRhuXoYYSd
+euMPqtp29S6U0bEXtTVdSEYzGKi/EaF5eNcRXUJcPl4aTCH7HTGmhsoCeQuKriU8
+JluGtPxJVji+z3JuIjXFmoxBY2KKb9mEosvXsNNDKvhrrVApSQf/SvDL3FDx9D81
+MAHjQknn1INvSk3M7IWV/dGL0tnDQMiCPUM6XT+RQuM03aCMfu/IvWuDDUNumWBU
+5xtBX0Iu2OybRUzL9kyWsXFSQx617v7tuXuPWg158Dg3RyCIC2VdwouzGGsJXslc
+W4T74w6HsxDpzxqrzwnwVHahn5/qntptFBlB37waGKRdvrTqlWq2jwHSp0F+y/x6
+h9BobBAJ7E6Ix5tprQSkPI6X62yXBfz2GDfZRYL2ltdD72mN5zzoA/SaT+HTtjOK
+wS2GPxb5IZkB7u0yZQOkQ3hDtI4Ve8rj8Z0mK1S0HETU9kvA6/+3KmA1xeMjvlzL
+HNmyIfXtfd5NIIrrRuWwGR2Y9KrezAdW+UuxHDE2SLDNCzJKjyQhzi8110Vtlm4A
+45UCAwEAAaNQME4wHQYDVR0OBBYEFNm1kQeEsxrCNeJA7q/67Pz43/mVMB8GA1Ud
+IwQYMBaAFNm1kQeEsxrCNeJA7q/67Pz43/mVMAwGA1UdEwQFMAMBAf8wDQYJKoZI
+hvcNAQELBQADggIBAJpQL0Wh6eP3XpzS+PyiSFAjtKLsbtkDId0NTSrUiARFSj7I
+S9YH1b0iG9pEd3XaNRffC72/R7UnYz0tbRC8tXYOKRB+gOkOAQXAleq01Qs93mA5
+6Titg8k9qXw6Vv+QK08CJUUFva6vMNOIbWkKN7GfdQZOig2EqLZ+jBdIai/NZ+uD
+US2vk87Lyd+8VQZHsazxLLS/hMyBvJdiFhQmjBpak9J1EisqGWjmRVvo1Vq1aVy8
+IrqW6GweWs0toFflbd57xFeV7+VJ1WDg6HU+JY5hWnTyH8HDQJFTY0GBoPA4gnxx
+c8P6VCuOO67bJPRxlI89+o7lRqfWXc+C2qQqdYTQda9850EwrTRl6dcHQZvZUYak
+DV+vxN+zHZ+lN3RmQMl+BY/sIuDqu3MunPt8c8KXeL0zGw8A3v6UBzMJAN4FbFX6
+Rc9jdCvqi+DqtMq7nGJ+V9swlfPSeg5kot/Z9xlmjqT0256PW6yaxRsXhhNTGM/f
+qCUmxf0aP3W8V6tCorSg6aiJ/xi0tLrfFDttgEH4CJHJh1n6w/D9aM9Q+ZRHTAlN
+JXUZyhyv7OoyA/gsHoL6g28AzuCYAmPLVNS4Ym1L9r4R2Ltq1b4em7aUfIMomgqQ
+xm+bOUujZtIJhDA8ckKKyb7xP0fJ13sTQyUt0/6CxJq6rj0OfVlQ3shGOBTU
+-----END CERTIFICATE-----