1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.security.net.config;
18 
19 import static android.security.Flags.certificateTransparencyConfiguration;
20 
21 import android.annotation.NonNull;
22 import android.util.Pair;
23 
24 import java.util.HashSet;
25 import java.util.Locale;
26 import java.util.Set;
27 
28 import javax.net.ssl.X509TrustManager;
29 
30 /**
31  * An application's network security configuration.
32  *
33  * <p>{@link #getConfigForHostname(String)} provides a means to obtain network security
34  * configuration to be used for communicating with a specific hostname.</p>
35  *
36  * @hide
37  */
38 public final class ApplicationConfig {
39     private static ApplicationConfig sInstance;
40     private static Object sLock = new Object();
41 
42     private Set<Pair<Domain, NetworkSecurityConfig>> mConfigs;
43     private NetworkSecurityConfig mDefaultConfig;
44     private X509TrustManager mTrustManager;
45 
46     private ConfigSource mConfigSource;
47     private boolean mInitialized;
48     private final Object mLock = new Object();
49 
ApplicationConfig(ConfigSource configSource)50     public ApplicationConfig(ConfigSource configSource) {
51         mConfigSource = configSource;
52         mInitialized = false;
53     }
54 
55     /**
56      * @hide
57      */
hasPerDomainConfigs()58     public boolean hasPerDomainConfigs() {
59         ensureInitialized();
60         return mConfigs != null && !mConfigs.isEmpty();
61     }
62 
63     /**
64      * Get the {@link NetworkSecurityConfig} corresponding to the provided hostname.
65      * When matching the most specific matching domain rule will be used, if no match exists
66      * then the default configuration will be returned.
67      *
68      * {@code NetworkSecurityConfig} objects returned by this method can be safely cached for
69      * {@code hostname}. Subsequent calls with the same hostname will always return the same
70      * {@code NetworkSecurityConfig}.
71      *
72      * @return {@link NetworkSecurityConfig} to be used to determine
73      * the network security configuration for connections to {@code hostname}.
74      */
getConfigForHostname(String hostname)75     public NetworkSecurityConfig getConfigForHostname(String hostname) {
76         ensureInitialized();
77         if (hostname == null || hostname.isEmpty() || mConfigs == null) {
78             return mDefaultConfig;
79         }
80         if (hostname.charAt(0) ==  '.') {
81             throw new IllegalArgumentException("hostname must not begin with a .");
82         }
83         // Domains are case insensitive.
84         hostname = hostname.toLowerCase(Locale.US);
85         // Normalize hostname by removing trailing . if present, all Domain hostnames are
86         // absolute.
87         if (hostname.charAt(hostname.length() - 1) == '.') {
88             hostname = hostname.substring(0, hostname.length() - 1);
89         }
90         // Find the Domain -> NetworkSecurityConfig entry with the most specific matching
91         // Domain entry for hostname.
92         // TODO: Use a smarter data structure for the lookup.
93         Pair<Domain, NetworkSecurityConfig> bestMatch = null;
94         for (Pair<Domain, NetworkSecurityConfig> entry : mConfigs) {
95             Domain domain = entry.first;
96             NetworkSecurityConfig config = entry.second;
97             // Check for an exact match.
98             if (domain.hostname.equals(hostname)) {
99                 return config;
100             }
101             // Otherwise check if the Domain includes sub-domains and that the hostname is a
102             // sub-domain of the Domain.
103             if (domain.subdomainsIncluded
104                     && hostname.endsWith(domain.hostname)
105                     && hostname.charAt(hostname.length() - domain.hostname.length() - 1) == '.') {
106                 if (bestMatch == null) {
107                     bestMatch = entry;
108                 } else if (domain.hostname.length() > bestMatch.first.hostname.length()) {
109                     bestMatch = entry;
110                 }
111             }
112         }
113         if (bestMatch != null) {
114             return bestMatch.second;
115         }
116         // If no match was found use the default configuration.
117         return mDefaultConfig;
118     }
119 
120     /**
121      * Returns the {@link X509TrustManager} that implements the checking of trust anchors and
122      * certificate pinning based on this configuration.
123      */
getTrustManager()124     public X509TrustManager getTrustManager() {
125         ensureInitialized();
126         return mTrustManager;
127     }
128 
129     /**
130      * Returns {@code true} if cleartext traffic is permitted for this application, which is the
131      * case only if all configurations permit cleartext traffic. For finer-grained policy use
132      * {@link #isCleartextTrafficPermitted(String)}.
133      */
isCleartextTrafficPermitted()134     public boolean isCleartextTrafficPermitted() {
135         ensureInitialized();
136         if (mConfigs != null) {
137             for (Pair<Domain, NetworkSecurityConfig> entry : mConfigs) {
138                 if (!entry.second.isCleartextTrafficPermitted()) {
139                     return false;
140                 }
141             }
142         }
143 
144         return mDefaultConfig.isCleartextTrafficPermitted();
145     }
146 
147     /**
148      * Returns {@code true} if cleartext traffic is permitted for this application when connecting
149      * to {@code hostname}.
150      */
isCleartextTrafficPermitted(String hostname)151     public boolean isCleartextTrafficPermitted(String hostname) {
152         return getConfigForHostname(hostname).isCleartextTrafficPermitted();
153     }
154 
155     /**
156      * Returns {@code true} if Certificate Transparency information is required to be verified by
157      * the client in TLS connections to {@code hostname}.
158      *
159      * <p>See RFC6962 section 3.3 for more details.
160      *
161      * @param hostname hostname to check whether certificate transparency verification is required
162      * @return {@code true} if certificate transparency verification is required and {@code false}
163      *     otherwise
164      */
isCertificateTransparencyVerificationRequired(@onNull String hostname)165     public boolean isCertificateTransparencyVerificationRequired(@NonNull String hostname) {
166         return certificateTransparencyConfiguration()
167                 ? getConfigForHostname(hostname).isCertificateTransparencyVerificationRequired()
168                 : NetworkSecurityConfig.DEFAULT_CERTIFICATE_TRANSPARENCY_VERIFICATION_REQUIRED;
169     }
170 
handleTrustStorageUpdate()171     public void handleTrustStorageUpdate() {
172         synchronized(mLock) {
173             // If the config is uninitialized then there is no work to be done to handle an update,
174             // avoid needlessly parsing configs.
175             if (!mInitialized) {
176                 return;
177             }
178             mDefaultConfig.handleTrustStorageUpdate();
179             if (mConfigs != null) {
180                 Set<NetworkSecurityConfig> updatedConfigs =
181                         new HashSet<NetworkSecurityConfig>(mConfigs.size());
182                 for (Pair<Domain, NetworkSecurityConfig> entry : mConfigs) {
183                     if (updatedConfigs.add(entry.second)) {
184                         entry.second.handleTrustStorageUpdate();
185                     }
186                 }
187             }
188         }
189     }
190 
ensureInitialized()191     private void ensureInitialized() {
192         synchronized(mLock) {
193             if (mInitialized) {
194                 return;
195             }
196             mConfigs = mConfigSource.getPerDomainConfigs();
197             mDefaultConfig = mConfigSource.getDefaultConfig();
198             mConfigSource = null;
199             mTrustManager = new RootTrustManager(this);
200             mInitialized = true;
201         }
202     }
203 
setDefaultInstance(ApplicationConfig config)204     public static void setDefaultInstance(ApplicationConfig config) {
205         synchronized (sLock) {
206             sInstance = config;
207         }
208     }
209 
getDefaultInstance()210     public static ApplicationConfig getDefaultInstance() {
211         synchronized (sLock) {
212             return sInstance;
213         }
214     }
215 }
216