Skip to content

Commit 98aa04a

Browse files
committed
DomainMatchRule and forDomains() support in MemorizingTrustManager
1 parent c095f29 commit 98aa04a

File tree

6 files changed

+403
-13
lines changed

6 files changed

+403
-13
lines changed

docs/ADVANCED_USAGE.markdown

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,15 @@ based on your supported API levels.
8484
These configuration methods are optional:
8585

8686
- `noTOFU()` is to disable automatic trust-on-first-use behavior, described
87-
below
87+
below.
8888

8989
- `cacheSize()` indicates how many domains' worth of memorized certificates
90-
should be held in cache (default: 128)
90+
should be held in cache (default: 128).
91+
92+
- `forDomains()` indicates which domains should be memorized; all other
93+
domains will be ignored, as if this `TrustManager` were not involved. The
94+
default is to memorize all domains. This method takes a `DomainMatchRule`,
95+
described in detail later in this page.
9196

9297
### Adding the MemorizingTrustManager
9398

@@ -210,6 +215,45 @@ instance that was outstanding at the time.
210215
The test suite for this library does use multiple `MemorizingTrustManager`
211216
instances, mostly to confirm that certificates do get loaded from disk.
212217

218+
### Memorizing Certain Domains
219+
220+
By default, `MemorizingTrustManager` applies for all domains that it sees.
221+
222+
Alternatively, you can use `forDomains()` to limit the scope of the domains
223+
that `MemorizingTrustManager` worries about. `forDomains()` takes a
224+
`DomainMatchRule` as a parameter.
225+
226+
The simplest ways to create a `DomainMatchRule` are the `whitelist()`
227+
and `blacklist()` static methods on that class. They each take one or more
228+
`String` parameters representing domains. Those can either be simple domains
229+
(e.g., `foo.com`) or with a leading wildcard (e.g., `*.foo.com`). `whitelist()`
230+
will apply the `MemorizingTrustManager` for the specified domains and skip
231+
it for anything else. `blacklist()` will skip the `MemorizingTrustManager`
232+
for the specified domains and apply it for anything else.
233+
234+
`DomainMatchRule` has other static methods for more granular control:
235+
236+
- `is(String)` takes a domain name or wildcard domain name and matches it
237+
but nothing else
238+
239+
- `is(Pattern)` takes a `Pattern` (i.e., regular expression) and matches
240+
it but nothing else
241+
242+
- `not(DomainMatchRule)` takes some other rule (e.g., one returned by `is()`)
243+
and inverts it
244+
245+
- `anyOf(DomainMatchRule...)` and `anyOf(List<DomainMatchRule>)` apply a logical
246+
OR, so a domain matching any of those rules is applied
247+
248+
- `allOf(DomainMatchRule...)` and `allOf(List<DomainMatchRule>)` apply a logical
249+
AND, so only domains matching all of those rules are applied
250+
251+
For example, `whitelist()` is implemented by wrapping each supplied domain
252+
in `is()` and using `anyOf()` for the collection, so we accept any of those
253+
domains but nothing else. `blacklist()` is implemented by wrapping each supplied
254+
domain in `not(is())` and using `allOf()` for the collection, so we only accept
255+
domains that are not any of the supplied ones.
256+
213257
## Integration with NetCipher
214258

215259
[NetCipher](https://github.com/guardianproject/NetCipher) is a library to

netsecurity/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ dependencies {
55
compile 'com.android.support:support-annotations:25.1.1'
66
provided 'com.squareup.okhttp3:okhttp:3.6.0'
77
androidTestCompile 'com.squareup.okhttp3:okhttp:3.6.0'
8+
testCompile 'junit:junit:4.12'
89
}
910

1011
android {

netsecurity/src/androidTest/java/com/commonsware/cwac/netsecurity/test/OkHttp3MemorizationTests.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import okhttp3.OkHttpClient;
2929
import okhttp3.Request;
3030
import okhttp3.Response;
31+
import static com.commonsware.cwac.netsecurity.DomainMatchRule.is;
3132

3233
@RunWith(AndroidJUnit4.class)
3334
public class OkHttp3MemorizationTests {
@@ -189,6 +190,23 @@ public void testTOFU() throws Exception {
189190
}
190191
}
191192

193+
@Test
194+
public void testDomainMatchRule() throws Exception {
195+
MemorizingTrustManager memo=new MemorizingTrustManager.Builder()
196+
.saveTo(memoDir, "sekrit".toCharArray())
197+
.noTOFU()
198+
.forDomains(is("this-so-does-not-exist.com"))
199+
.build();
200+
201+
final TrustManagerBuilder tmb=new TrustManagerBuilder().add(memo);
202+
203+
OkHttp3Integrator.applyTo(tmb, builder);
204+
OkHttpClient client=builder.build();
205+
206+
Response response=client.newCall(buildRequest()).execute();
207+
Assert.assertEquals(getExpectedResponse(), response.body().string());
208+
}
209+
192210
@Test
193211
public void testOr() throws Exception {
194212
MemorizingTrustManager memo=new MemorizingTrustManager.Builder()
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
/***
2+
Copyright (c) 2017 CommonsWare, LLC
3+
4+
Licensed under the Apache License, Version 2.0 (the "License"); you may
5+
not use this file except in compliance with the License. You may obtain
6+
a copy of the License at
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
Unless required by applicable law or agreed to in writing, software
9+
distributed under the License is distributed on an "AS IS" BASIS,
10+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
See the License for the specific language governing permissions and
12+
limitations under the License.
13+
*/
14+
15+
package com.commonsware.cwac.netsecurity;
16+
17+
import java.util.ArrayList;
18+
import java.util.Arrays;
19+
import java.util.List;
20+
import java.util.regex.Pattern;
21+
22+
/**
23+
* Represents a rule for identifying matching domain names. This is used
24+
* by MemorizingTrustManager to limit the scope of memorization to only
25+
* domains matching a rule.
26+
*
27+
* The whitelist() and blacklist() static methods implement two common
28+
* patterns (accept only domains in a list or deny a list of domains and
29+
* accept everything else). The other static methods allow you to assemble
30+
* other scenarios. These methods are designed to be used as static imports,
31+
* akin to Hamcrest matchers.
32+
*/
33+
abstract public class DomainMatchRule {
34+
abstract public boolean matches(String host);
35+
36+
/**
37+
* Implements a logical AND: only domains matching all of the supplied
38+
* rules are considered to be a match.
39+
*
40+
* @param rules Rules to apply and AND together
41+
* @return Rule implementing the AND logic
42+
*/
43+
public static DomainMatchRule allOf(DomainMatchRule... rules) {
44+
return(new Composite(false, rules));
45+
}
46+
47+
/**
48+
* Implements a logical AND: only domains matching all of the supplied
49+
* rules are considered to be a match.
50+
*
51+
* @param rules Rules to apply and AND together
52+
* @return Rule implementing the AND logic
53+
*/
54+
public static DomainMatchRule allOf(List<DomainMatchRule> rules) {
55+
return(new Composite(false, rules));
56+
}
57+
58+
/**
59+
* Implements a logical OR: any domains matching at least one of the supplied
60+
* rules are considered to be a match.
61+
*
62+
* @param rules Rules to apply and OR together
63+
* @return Rule implementing the OR logic
64+
*/
65+
public static DomainMatchRule anyOf(DomainMatchRule... rules) {
66+
return(new Composite(true, rules));
67+
}
68+
69+
/**
70+
* Implements a logical OR: any domains matching at least one of the supplied
71+
* rules are considered to be a match.
72+
*
73+
* @param rules Rules to apply and OR together
74+
* @return Rule implementing the OR logic
75+
*/
76+
public static DomainMatchRule anyOf(List<DomainMatchRule> rules) {
77+
return(new Composite(true, rules));
78+
}
79+
80+
/**
81+
* Inverts a rule, by applying a logical NOT to whatever it returns
82+
* from matches()
83+
*
84+
* @param rule Rules to invert
85+
* @return Rule implementing the NOT logic
86+
*/
87+
public static DomainMatchRule not(DomainMatchRule rule) {
88+
return(new Not(rule));
89+
}
90+
91+
/**
92+
* A concrete matcher, comparing domain names against the supplied
93+
* regular expression.
94+
*
95+
* @param pattern Pattern to compare against domain names
96+
* @return Rule implementing the pattern-match rule
97+
*/
98+
public static DomainMatchRule is(Pattern pattern) {
99+
return(new Regex(pattern));
100+
}
101+
102+
/**
103+
* A concrete matcher, comparing domain names against a "glob"-style
104+
* expression. So, "foo.com" as a glob will only match "foo.com" as a
105+
* domain. But "*.foo.com" as a glob will match "www.foo.com", "bar.foo.com",
106+
* "www.bar.foo.com", and so on.
107+
*
108+
* @param glob Glob to compare against domain names
109+
* @return Rule implementing the glob-match rule
110+
*/
111+
public static DomainMatchRule is(String glob) {
112+
return(new Regex(glob));
113+
}
114+
115+
/**
116+
* Accept any of the supplied globs, and reject anything else (see is(String)
117+
* for what "glob" means).
118+
*
119+
* @param globs Globs to compare against domain names
120+
* @return Rule implementing the whitelist
121+
*/
122+
public static DomainMatchRule whitelist(String... globs) {
123+
List<DomainMatchRule> rules=new ArrayList<>();
124+
125+
for (String glob : globs) {
126+
rules.add(is(glob));
127+
}
128+
129+
return(anyOf(rules));
130+
}
131+
132+
/**
133+
* Reject all of the supplied globs, and accept anything else (see is(String)
134+
* for what "glob" means).
135+
*
136+
* @param globs Globs to compare against domain names
137+
* @return Rule implementing the blacklist
138+
*/
139+
public static DomainMatchRule blacklist(String... globs) {
140+
List<DomainMatchRule> rules=new ArrayList<>();
141+
142+
for (String glob : globs) {
143+
rules.add(not(is(glob)));
144+
}
145+
146+
return(allOf(rules));
147+
}
148+
149+
private static class Composite extends DomainMatchRule {
150+
private final List<DomainMatchRule> rules;
151+
private final boolean isOr;
152+
153+
Composite(boolean isOr, DomainMatchRule... rules) {
154+
this(isOr, Arrays.asList(rules));
155+
}
156+
157+
Composite(boolean isOr, List<DomainMatchRule> rules) {
158+
this.isOr=isOr;
159+
this.rules=rules;
160+
}
161+
162+
@Override
163+
public boolean matches(String host) {
164+
for (DomainMatchRule rule : rules) {
165+
boolean match=rule.matches(host);
166+
167+
if (match && isOr) {
168+
return(true);
169+
}
170+
else if (!match && !isOr) {
171+
return(false);
172+
}
173+
}
174+
175+
return(!isOr);
176+
}
177+
}
178+
179+
private static class Not extends DomainMatchRule {
180+
private final DomainMatchRule rule;
181+
182+
Not(DomainMatchRule rule) {
183+
this.rule=rule;
184+
}
185+
186+
@Override
187+
public boolean matches(String host) {
188+
return(!rule.matches(host));
189+
}
190+
}
191+
192+
private static class Regex extends DomainMatchRule {
193+
private final Pattern pattern;
194+
195+
Regex(String glob) {
196+
this(Pattern.compile(glob.replaceAll("\\.", "\\\\.")
197+
.replaceAll("\\*", "\\.\\*")));
198+
}
199+
200+
Regex(Pattern pattern) {
201+
this.pattern=pattern;
202+
}
203+
204+
@Override
205+
public boolean matches(String host) {
206+
return(pattern.matcher(host).matches());
207+
}
208+
}
209+
}

netsecurity/src/main/java/com/commonsware/cwac/netsecurity/MemorizingTrustManager.java

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -58,15 +58,17 @@ public class MemorizingTrustManager implements X509Extensions {
5858
private final String storeType;
5959
private final boolean noTOFU;
6060
private final LruCache<String, MemorizingStore> stores;
61+
private final DomainMatchRule domainMatchRule;
6162

6263
private MemorizingTrustManager(File workingDir, char[] storePassword,
6364
String storeType, boolean noTOFU,
64-
int cacheSize) {
65+
int cacheSize, DomainMatchRule domainMatchRule) {
6566
this.workingDir=workingDir;
6667
this.storePassword=storePassword;
6768
this.storeType=storeType;
6869
this.noTOFU=noTOFU;
6970
this.stores=new LruCache<>(cacheSize);
71+
this.domainMatchRule=domainMatchRule;
7072
}
7173

7274
/*
@@ -105,16 +107,18 @@ public List<X509Certificate> checkServerTrusted(
105107
@NonNull X509Certificate[] chain, String authType, String host)
106108
throws CertificateException {
107109

108-
try {
109-
getStoreForHost(host).checkServerTrusted(chain, authType);
110-
}
111-
catch (Exception e) {
112-
if (e instanceof CertificateNotMemorizedException ||
113-
e instanceof MemorizationMismatchException) {
114-
throw (CertificateException)e;
110+
if (domainMatchRule==null || domainMatchRule.matches(host)) {
111+
try {
112+
getStoreForHost(host).checkServerTrusted(chain, authType);
115113
}
116-
else {
117-
throw new CertificateException("Exception setting up memoization", e);
114+
catch (Exception e) {
115+
if (e instanceof CertificateNotMemorizedException ||
116+
e instanceof MemorizationMismatchException) {
117+
throw (CertificateException)e;
118+
}
119+
else {
120+
throw new CertificateException("Exception setting up memoization", e);
121+
}
118122
}
119123
}
120124

@@ -243,6 +247,7 @@ public static class Builder {
243247
private String storeType;
244248
private boolean noTOFU=false;
245249
private int cacheSize=128;
250+
private DomainMatchRule domainMatchRule;
246251

247252
/**
248253
* Indicates where the keystores associated with memorize() should
@@ -315,6 +320,12 @@ public Builder cacheSize(int cacheSize) {
315320
return(this);
316321
}
317322

323+
public Builder forDomains(DomainMatchRule domainMatchRule) {
324+
this.domainMatchRule=domainMatchRule;
325+
326+
return(this);
327+
}
328+
318329
/**
319330
* Validates your configuration and builds the MemorizingTrustManager.
320331
*
@@ -335,7 +346,7 @@ public MemorizingTrustManager build() {
335346
workingDir.mkdirs();
336347

337348
return(new MemorizingTrustManager(workingDir, storePassword, storeType,
338-
noTOFU, cacheSize));
349+
noTOFU, cacheSize, domainMatchRule));
339350
}
340351
}
341352

0 commit comments

Comments
 (0)