-
Notifications
You must be signed in to change notification settings - Fork 92
/
TrustKit.java
395 lines (363 loc) · 18.3 KB
/
TrustKit.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
package com.datatheorem.android.trustkit;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import androidx.annotation.NonNull;
import android.util.Printer;
import com.datatheorem.android.trustkit.config.ConfigurationException;
import com.datatheorem.android.trustkit.config.TrustKitConfiguration;
import com.datatheorem.android.trustkit.pinning.TrustManagerBuilder;
import com.datatheorem.android.trustkit.reporting.BackgroundReporter;
import com.datatheorem.android.trustkit.utils.TrustKitLog;
import com.datatheorem.android.trustkit.utils.VendorIdentifier;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.util.Set;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
/**
* Class that provides all of the TrustKit public APIs.
*
* <p>
* It should be used to initialize the App's SSL pinning policy and to retrieve the
* corresponding {@code SSLSocketFactory} and {@code X509TrustManager}, to be used to add SSL
* pinning validation to the App's network connections.
* </p>
*
* <p>
* TrustKit works by extending the
* <a href="https://developer.android.com/training/articles/security-config.html" target="_blank">
* Android N Network Security Configuration</a> in two ways:
*
* <ul>
* <li>It provides support for the SSL pinning functionality of the Android N Network
* Security Configuration to earlier versions of Android, down to API level 17. This
* allows Apps supporting versions of Android that earlier than N to implement SSL
* pinning in a way that is future-proof.</li>
*
* <li>It adds the ability to send reports when pinning validation failed for a specific
* connection. Reports have a format that is similar to the report-uri feature of
* <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Public_Key_Pinning" target="_blank">HTTP
* Public Key Pinning</a> and <a href="https://github.com/datatheorem/trustkit" target="_blank">TrustKit
* iOS</a>.</li>
* </ul>
*
* For better compatibility, TrustKit will also run on API levels 15 and 16 but its
* functionality will be disabled.
* </p>
*
* <h3>Supported Android N Network Security Settings</h3>
*
* <p>
* On devices before Android N, TrustKit supports the following XML tags defined in the
* <a href="https://developer.android.com/training/articles/security-config.html#CertificatePinning" target="_blank">
* Android N Network Security Configuration</a> for deploying SSL pinning:
* </p>
*
* <ul>
* <li>{@code <domain-config>}.</li>
* <li>{@code <domain>} and the {@code includeSubdomains} attribute.</li>
* <li>{@code <pin-set>} and the {@code expiration} attribute.</li>
* <li>{@code <pin>} and the {@code digest} attribute.</li>
* <li>{@code <debug-overrides>}.</li>
* <li>{@code <trust-anchors>}, but only within a {@code <debug-overrides>} tag. Hence, custom
* trust anchors for specific domains cannot be set.</li>
* <li>{@code <certificates>} and the {@code overridePins} and {@code src} attributes. Only raw
* certificate files are supported for the {@code src} attribute ({@code user} and
* {@code system} values will be ignored).</li>
* </ul>
*
*<p>
* On Android N devices, the OS' implementation is used and all XML tags are supported.
*</p>
*
* <h3>Additional TrustKit Settings</h3>
*
* <p>
* TrustKit provides additional functionality to not enforce pinning validation and to allow
* reports to be sent by the App whenever a pinning validation failure occurred.
* </p>
*
* <h4>{@code <trustkit-config>}</h4>
*
* <p>
* The main tag for specifying additional TrustKit settings, to be defined within a
* {@code <domain-config>} entry. It supports the following attributes:
* </p>
*
* <ul>
* <li>{@code enforcePinning}: if set to {@code false}, TrustKit will not block SSL
* connections that caused a pinning validation error; default value is {@code false}. When
* a pinning failure occurs, pin failure reports will always be sent to the configured
* report URIs regardless of the value of {@code enforcePinning}. This behavior allows
* deploying pinning validation without the risk of locking out users due to a
* misconfiguration, while still receiving reports in order to assess how many users would
* be affected by pinning.</li>
*
* <li>{@code disableDefaultReportUri}: if set to {@code true}, the default report URL for
* sending pin failure reports will be disabled; default value is {@code false}. By default,
* pin failure reports are sent to a report server hosted by Data Theorem, for detecting
* potential CA compromises and man-in-the-middle attacks, as well as providing a free
* dashboard for developers; email
* <a href="mailto:info@datatheorem.com">info@datatheorem.com</a> if you'd like a dashboard
* for your App. Only pin failure reports are sent, which contain the App's package name,
* a randomly-generated ID, and the server's hostname and certificate chain that failed
* validation.</li>
* </ul>
*
* <h4>{@code <report-uri>}</h4>
*
* A URL to which pin validation failures should be reported, to be defined within a
* {@code <trustkit-config>} tag. The format of the reports is similar to the one described in
* <a href="https://tools.ietf.org/html/rfc7469#section-2.1.4" target="_blank">RFC 7469 for the HPKP
* specification</a>. A sample TrustKit report is available
* <a href="https://github.com/datatheorem/TrustKit-Android/blob/master/docs/sample_report.json" target="_blank">
* in the project's repository
* </a>.
*
* <h3>Sample TrustKit Configuration</h3>
* <p>
* The following configuration will pin the www.datatheorem.com domain without enforcing pinning
* validation, and will send pinning failure reports to report.datatheorem.com. It also defines
* a debug overrides to add a debug certificate authority to the App's trust store for easier
* debugging of the App's network traffic.
* </p>
* <pre>
* {@code
* <!-- res/xml/network_security_config.xml -->
* <?xml version="1.0" encoding="utf-8"?>
* <network-security-config>
* <!-- Pin the domain www.datatheorem.com -->
* <!-- Official Android N API -->
* <domain-config>
* <domain>www.datatheorem.com</domain>
* <pin-set>
* <pin digest="SHA-256">k3XnEYQCK79AtL9GYnT/nyhsabas03V+bhRQYHQbpXU=</pin>
* <pin digest="SHA-256">2kOi4HdYYsvTR1sTIR7RHwlf2SescTrpza9ZrWy7poQ=</pin>
* </pin-set>
* <!-- TrustKit Android API -->
* <!-- Do not enforce pinning validation -->
* <trustkit-config enforcePinning="false">
* <!-- Add a reporting URL for pin validation reports -->
* <report-uri>http://report.datatheorem.com/log_report</report-uri>
* </trustkit-config>
* </domain-config>
* <debug-overrides>
* <trust-anchors>
* <!-- For debugging purposes, add a debug CA and override pins -->
* <certificates overridePins="true" src="@raw/debugca" />
* </trust-anchors>
* </debug-overrides>
* </network-security-config>
* }
* </pre>
*
*/
public class TrustKit {
protected static TrustKit trustKitInstance;
private final TrustKitConfiguration trustKitConfiguration;
protected TrustKit(@NonNull Context context,
@NonNull TrustKitConfiguration trustKitConfiguration) {
this.trustKitConfiguration = trustKitConfiguration;
// Setup the debug-overrides setting if the App is debuggable
// Do not use BuildConfig.DEBUG as it does not work for libraries
boolean isAppDebuggable = (0 !=
(context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE));
Set<Certificate> debugCaCerts = null;
boolean shouldOverridePins = false;
if (isAppDebuggable) {
debugCaCerts = trustKitConfiguration.getDebugCaCertificates();
if (debugCaCerts != null) {
TrustKitLog.i("App is debuggable - processing <debug-overrides> configuration.");
}
shouldOverridePins = trustKitConfiguration.shouldOverridePins();
}
// Create the background reporter for sending pin failure reports
String appPackageName = context.getPackageName();
String appVersion;
try {
appVersion = context.getPackageManager().getPackageInfo(appPackageName, 0).versionName;
} catch (PackageManager.NameNotFoundException e) {
appVersion = "N/A";
}
if (appVersion == null) {
appVersion = "N/A";
}
String appVendorId = VendorIdentifier.getOrCreate(context);
BackgroundReporter reporter = new BackgroundReporter(context, appPackageName, appVersion,
appVendorId);
// Initialize the trust manager builder
try {
TrustManagerBuilder.initializeBaselineTrustManager(debugCaCerts,
shouldOverridePins, reporter);
} catch (CertificateException | NoSuchAlgorithmException | KeyStoreException
| IOException e) {
throw new ConfigurationException("Could not parse <debug-overrides> certificates");
}
}
/** Try to retrieve the Network Security Policy resource ID configured in the App's manifest.
* Somewhat convoluted as other means of getting the resource ID involve using private APIs.
*
* @param context android context
* @return The resource ID for the XML file containing the configured Network Security Policy or
* -1 if no policy was configured in the App's manifest or if we are not running on Android N.
*/
static private int getNetSecConfigResourceId(@NonNull Context context) {
ApplicationInfo info = context.getApplicationInfo();
// Dump the content of the ApplicationInfo, which contains the resource ID on Android N
class NetSecConfigResIdRetriever implements Printer {
private int netSecConfigResourceId = -1;
public void println(String x) {
if (netSecConfigResourceId == -1) {
// Attempt at parsing "networkSecurityConfigRes=0x1234"
String NETSEC_LINE_FORMAT = "networkSecurityConfigRes=0x";
if (x.contains(NETSEC_LINE_FORMAT)) {
netSecConfigResourceId =
Integer.parseInt(x.substring(NETSEC_LINE_FORMAT.length()), 16);
}
}
}
private int getNetworkSecurityConfigResId() { return netSecConfigResourceId; }
}
NetSecConfigResIdRetriever retriever = new NetSecConfigResIdRetriever();
info.dump(retriever, "");
return retriever.getNetworkSecurityConfigResId();
}
/** Initialize TrustKit with the Network Security Configuration file at the default location
* res/xml/network_security_config.xml. The Network Security Configuration file must also have
* been <a href="https://developer.android.com/training/articles/security-config.html#manifest" target="_blank">
* added to the App's manifest</a>.
*
* @param context the application's context.
* @throws ConfigurationException if the policy could not be parsed or contained errors.
*/
@NonNull
public synchronized static TrustKit initializeWithNetworkSecurityConfiguration(
@NonNull Context context) {
// Try to get the default network policy resource ID
int networkSecurityConfigId = context.getResources().getIdentifier(
"network_security_config", "xml", context.getPackageName());
return initializeWithNetworkSecurityConfiguration(context, networkSecurityConfigId);
}
/** Initialize TrustKit with the Network Security Configuration file with the specified
* resource ID. The Network Security Configuration file must also have
* been <a href="https://developer.android.com/training/articles/security-config.html#manifest" target="_blank">
* added to the App's manifest</a>.
*
* @param context the application's context.
* @param configurationResourceId the resource ID for the Network Security Configuration file to
* use.
* @throws ConfigurationException if the policy could not be parsed or contained errors.
*/
@NonNull
public synchronized static TrustKit initializeWithNetworkSecurityConfiguration(
@NonNull Context context, int configurationResourceId) {
if (trustKitInstance != null) {
throw new IllegalStateException("TrustKit has already been initialized");
}
// On Android N, ensure that the system was also able to load the policy
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// This will need to be updated/double-checked for subsequent versions of Android
int systemConfigResId = getNetSecConfigResourceId(context);
if (systemConfigResId == -1) {
// Android did not find a policy because the supplied resource ID is wrong or the
// policy file is not properly setup in the manifest, or contains bad data
throw new ConfigurationException("TrustKit was initialized with a network policy " +
"that was not properly configured for Android N - make sure it is in the " +
"App's Manifest.");
}
else if (systemConfigResId != configurationResourceId) {
throw new ConfigurationException("TrustKit was initialized with a different " +
"network policy than the one configured in the App's manifest.");
}
}
// Then try to load the supplied policy
TrustKitConfiguration trustKitConfiguration;
try {
trustKitConfiguration = TrustKitConfiguration.fromXmlPolicy(
context, context.getResources().getXml(configurationResourceId)
);
} catch (XmlPullParserException | IOException e) {
throw new ConfigurationException("Could not parse network security policy file");
} catch (CertificateException e) {
throw new ConfigurationException("Could not find the debug certificate in the " +
"network security police file");
}
trustKitInstance = new TrustKit(context, trustKitConfiguration);
return trustKitInstance;
}
/** Retrieve the initialized instance of TrustKit.
*
* @throws IllegalStateException if TrustKit has not been initialized.
*/
@NonNull
public static TrustKit getInstance() {
if (trustKitInstance == null) {
throw new IllegalStateException("TrustKit has not been initialized");
}
return trustKitInstance;
}
/** Retrieve the current TrustKit configuration.
*
*/
@NonNull
public TrustKitConfiguration getConfiguration() { return trustKitConfiguration; }
/** Retrieve an {@code SSLSSocketFactory} that implements SSL pinning validation based on the
* current TrustKit configuration for the specified serverHostname. It can be used with most
* network APIs (such as {@code HttpsUrlConnection}) to add SSL pinning validation to the
* connections.
*
* <p>
* The {@code SSLSocketFactory} is configured for the supplied serverHostname, and will
* enforce this domain's pinning policy even if a redirection to a different domain occurs
* during the connection. Hence validation will always fail in the case of a redirection to
* a different domain.
* However, pinning validation is only meant to be used on the App's API server(s), and
* redirections to other domains should not happen in this scenario.
* </p>
*
* @param serverHostname the server's hostname that the {@code SSLSocketFactory} will be used to
* connect to. This hostname will be used to retrieve the pinning policy
* from the current TrustKit configuration.
*/
@NonNull
public SSLSocketFactory getSSLSocketFactory(@NonNull String serverHostname) {
try {
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[]{getTrustManager(serverHostname)}, null);
return sslContext.getSocketFactory();
} catch (NoSuchAlgorithmException | KeyManagementException e) {
e.printStackTrace();
throw new IllegalStateException("Should not happen");
}
}
/** Retrieve an {@code X509TrustManager} that implements SSL pinning validation based on the
* current TrustKit configuration for the supplied hostname. It can be used with some network
* APIs that let developers supply a trust manager to customize SSL validation.
*
* <p>
* The {@code X509TrustManager} is configured for the supplied serverHostname, and will
* enforce this domain's pinning policy even if a redirection to a different domain occurs
* during the connection. Hence validation will always fail in the case of a redirection to
* a different domain.
* However, pinning validation is only meant to be used on the App's API server(s), and
* redirections to other domains should not happen in this scenario.
* </p>
*
* @param serverHostname the server's hostname that the {@code X509TrustManager} will be used to
* connect to. This hostname will be used to retrieve the pinning policy
* from the current TrustKit configuration.
*/
@NonNull
public X509TrustManager getTrustManager(@NonNull String serverHostname) {
return TrustManagerBuilder.getTrustManager(serverHostname);
}
}