• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 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 com.android.server.wifi;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.annotation.SuppressLint;
22 import android.app.Notification;
23 import android.app.PendingIntent;
24 import android.content.BroadcastReceiver;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.content.IntentFilter;
28 import android.graphics.drawable.Icon;
29 import android.net.Uri;
30 import android.net.wifi.WifiConfiguration;
31 import android.net.wifi.WifiContext;
32 import android.net.wifi.WifiEnterpriseConfig;
33 import android.os.Handler;
34 import android.text.TextUtils;
35 import android.text.format.DateFormat;
36 import android.util.Log;
37 
38 import com.android.internal.annotations.VisibleForTesting;
39 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
40 import com.android.internal.util.HexDump;
41 import com.android.server.wifi.util.CertificateSubjectInfo;
42 import com.android.wifi.resources.R;
43 
44 import java.security.InvalidAlgorithmParameterException;
45 import java.security.KeyStore;
46 import java.security.MessageDigest;
47 import java.security.NoSuchAlgorithmException;
48 import java.security.cert.CertPath;
49 import java.security.cert.CertPathValidator;
50 import java.security.cert.CertPathValidatorException;
51 import java.security.cert.CertificateEncodingException;
52 import java.security.cert.CertificateException;
53 import java.security.cert.CertificateFactory;
54 import java.security.cert.PKIXParameters;
55 import java.security.cert.TrustAnchor;
56 import java.security.cert.X509Certificate;
57 import java.util.Date;
58 import java.util.Enumeration;
59 import java.util.LinkedList;
60 import java.util.Set;
61 import java.util.StringJoiner;
62 
63 /** This class is used to handle insecure EAP networks. */
64 public class InsecureEapNetworkHandler {
65     private static final String TAG = "InsecureEapNetworkHandler";
66 
67     @VisibleForTesting
68     static final String ACTION_CERT_NOTIF_TAP =
69             "com.android.server.wifi.ClientModeImpl.ACTION_CERT_NOTIF_TAP";
70     @VisibleForTesting
71     static final String ACTION_CERT_NOTIF_ACCEPT =
72             "com.android.server.wifi.ClientModeImpl.ACTION_CERT_NOTIF_ACCEPT";
73     @VisibleForTesting
74     static final String ACTION_CERT_NOTIF_REJECT =
75             "com.android.server.wifi.ClientModeImpl.ACTION_CERT_NOTIF_REJECT";
76     @VisibleForTesting
77     static final String EXTRA_PENDING_CERT_SSID =
78             "com.android.server.wifi.ClientModeImpl.EXTRA_PENDING_CERT_SSID";
79 
80     static final String TOFU_ANONYMOUS_IDENTITY = "anonymous";
81     private final String mCaCertHelpLink;
82     private final WifiContext mContext;
83     private final WifiConfigManager mWifiConfigManager;
84     private final WifiNative mWifiNative;
85     private final FrameworkFacade mFacade;
86     private final WifiNotificationManager mNotificationManager;
87     private final WifiDialogManager mWifiDialogManager;
88     private final boolean mIsTrustOnFirstUseSupported;
89     private final boolean mIsInsecureEnterpriseConfigurationAllowed;
90     private final InsecureEapNetworkHandlerCallbacks mCallbacks;
91     private final String mInterfaceName;
92     private final Handler mHandler;
93     private final OnNetworkUpdateListener mOnNetworkUpdateListener;
94 
95     // The latest connecting configuration from the caller, it is updated on calling
96     // prepareConnection() always. This is used to ensure that current TOFU config is aligned
97     // with the caller connecting config.
98     @NonNull
99     private WifiConfiguration mConnectingConfig = null;
100     // The connecting configuration which is a valid TOFU configuration, it is updated
101     // only when the connecting configuration is a valid TOFU configuration and used
102     // by later TOFU procedure.
103     @NonNull
104     private WifiConfiguration mCurrentTofuConfig = null;
105     private int mPendingRootCaCertDepth = -1;
106     @Nullable
107     private X509Certificate mPendingRootCaCert = null;
108     @Nullable
109     private X509Certificate mPendingServerCert = null;
110     // This is updated on setting a pending server cert.
111     private CertificateSubjectInfo mPendingServerCertSubjectInfo = null;
112     // This is updated on setting a pending server cert.
113     private CertificateSubjectInfo mPendingServerCertIssuerInfo = null;
114     // Record the whole server cert chain from Root CA to the server cert.
115     // The order of the certificates in the chain required by the validation method is in the
116     // reverse order to the order we receive them from the lower layers. Therefore, we are using a
117     // LinkedList data type here, so that we could add certificates to the head, rather than
118     // using an ArrayList and then having to reverse it.
119     // Using SuppressLint here to avoid linter errors related to LinkedList usage.
120     @SuppressLint("JdkObsolete")
121     private LinkedList<X509Certificate> mServerCertChain = new LinkedList<>();
122     private WifiDialogManager.DialogHandle mTofuAlertDialog = null;
123     private boolean mIsCertNotificationReceiverRegistered = false;
124     private String mServerCertHash = null;
125     private boolean mUseTrustStore;
126 
127     BroadcastReceiver mCertNotificationReceiver = new BroadcastReceiver() {
128         @Override
129         public void onReceive(Context context, Intent intent) {
130             String action = intent.getAction();
131             String ssid = intent.getStringExtra(EXTRA_PENDING_CERT_SSID);
132             // This is an onGoing notification, dismiss it once an action is sent.
133             dismissDialogAndNotification();
134             Log.d(TAG, "Received CertNotification: ssid=" + ssid + ", action=" + action);
135             if (TextUtils.equals(action, ACTION_CERT_NOTIF_TAP)) {
136                 askForUserApprovalForCaCertificate();
137             } else if (TextUtils.equals(action, ACTION_CERT_NOTIF_ACCEPT)) {
138                 handleAccept(ssid);
139             } else if (TextUtils.equals(action, ACTION_CERT_NOTIF_REJECT)) {
140                 handleReject(ssid);
141             }
142         }
143     };
144 
InsecureEapNetworkHandler(@onNull WifiContext context, @NonNull WifiConfigManager wifiConfigManager, @NonNull WifiNative wifiNative, @NonNull FrameworkFacade facade, @NonNull WifiNotificationManager notificationManager, @NonNull WifiDialogManager wifiDialogManager, boolean isTrustOnFirstUseSupported, boolean isInsecureEnterpriseConfigurationAllowed, @NonNull InsecureEapNetworkHandlerCallbacks callbacks, @NonNull String interfaceName, @NonNull Handler handler)145     public InsecureEapNetworkHandler(@NonNull WifiContext context,
146             @NonNull WifiConfigManager wifiConfigManager,
147             @NonNull WifiNative wifiNative,
148             @NonNull FrameworkFacade facade,
149             @NonNull WifiNotificationManager notificationManager,
150             @NonNull WifiDialogManager wifiDialogManager,
151             boolean isTrustOnFirstUseSupported,
152             boolean isInsecureEnterpriseConfigurationAllowed,
153             @NonNull InsecureEapNetworkHandlerCallbacks callbacks,
154             @NonNull String interfaceName,
155             @NonNull Handler handler) {
156         mContext = context;
157         mWifiConfigManager = wifiConfigManager;
158         mWifiNative = wifiNative;
159         mFacade = facade;
160         mNotificationManager = notificationManager;
161         mWifiDialogManager = wifiDialogManager;
162         mIsTrustOnFirstUseSupported = isTrustOnFirstUseSupported;
163         mIsInsecureEnterpriseConfigurationAllowed = isInsecureEnterpriseConfigurationAllowed;
164         mCallbacks = callbacks;
165         mInterfaceName = interfaceName;
166         mHandler = handler;
167 
168         mOnNetworkUpdateListener = new OnNetworkUpdateListener();
169         mWifiConfigManager.addOnNetworkUpdateListener(mOnNetworkUpdateListener);
170 
171         mCaCertHelpLink = mContext.getString(R.string.config_wifiCertInstallationHelpLink);
172     }
173 
174     /**
175      * Prepare TOFU data for a new connection.
176      *
177      * Prepare TOFU data if this is an Enterprise configuration, which
178      * uses Server Cert, without a valid Root CA certificate or user approval.
179      * If TOFU is supported and enabled, this method will also clear the user credentials in the
180      * initial connection to the server.
181      *
182      * @param config the running wifi configuration.
183      */
prepareConnection(@onNull WifiConfiguration config)184     public void prepareConnection(@NonNull WifiConfiguration config) {
185         if (null == config) return;
186         mConnectingConfig = config;
187 
188         if (!config.isEnterprise()) return;
189         WifiEnterpriseConfig entConfig = config.enterpriseConfig;
190         if (!entConfig.isEapMethodServerCertUsed()) return;
191         if (entConfig.hasCaCertificate()) return;
192 
193         Log.d(TAG, "prepareConnection: isTofuSupported=" + mIsTrustOnFirstUseSupported
194                 + ", isInsecureEapNetworkAllowed=" + mIsInsecureEnterpriseConfigurationAllowed
195                 + ", isTofuEnabled=" + entConfig.isTrustOnFirstUseEnabled()
196                 + ", isUserApprovedNoCaCert=" + entConfig.isUserApproveNoCaCert());
197         // If TOFU is not supported or insecure EAP network is allowed without TOFU enabled,
198         // skip the entire TOFU logic if this network was approved earlier by the user.
199         if (entConfig.isUserApproveNoCaCert()) {
200             if (!mIsTrustOnFirstUseSupported) return;
201             if (mIsInsecureEnterpriseConfigurationAllowed
202                     && !entConfig.isTrustOnFirstUseEnabled()) {
203                 return;
204             }
205         }
206 
207         if (mIsTrustOnFirstUseSupported && (entConfig.isTrustOnFirstUseEnabled()
208                 || !mIsInsecureEnterpriseConfigurationAllowed)) {
209             /**
210              * Clear the user credentials from this copy of the configuration object.
211              * Supplicant will start the phase-1 TLS session to acquire the server certificate chain
212              * which will be provided to the framework. Then since the callbacks for identity and
213              * password requests are not populated, it will fail the connection and disconnect.
214              * This will allow the user to review the certificates at their own pace, and a
215              * reconnection would automatically take place with full verification of the chain once
216              * they approve.
217              */
218             if (config.enterpriseConfig.getEapMethod() == WifiEnterpriseConfig.Eap.TTLS
219                     || config.enterpriseConfig.getEapMethod() == WifiEnterpriseConfig.Eap.PEAP) {
220                 config.enterpriseConfig.setPhase2Method(WifiEnterpriseConfig.Phase2.NONE);
221                 config.enterpriseConfig.setIdentity(null);
222                 if (TextUtils.isEmpty(config.enterpriseConfig.getAnonymousIdentity())) {
223                     /**
224                      * If anonymous identity was not provided, use "anonymous" to prevent any
225                      * untrusted server from tracking real user identities.
226                      */
227                     config.enterpriseConfig.setAnonymousIdentity(TOFU_ANONYMOUS_IDENTITY);
228                 }
229                 config.enterpriseConfig.setPassword(null);
230             }
231             if (mWifiNative.isSupplicantAidlServiceVersionAtLeast(2)) {
232                 // For AIDL v2+, we can start with the default trust store
233                 config.enterpriseConfig.setCaPath(WifiConfigurationUtil.getSystemTrustStorePath());
234             }
235         }
236         mCurrentTofuConfig = config;
237         mServerCertChain.clear();
238         dismissDialogAndNotification();
239         registerCertificateNotificationReceiver();
240 
241         if (useTrustOnFirstUse()) {
242             // Remove cached PMK in the framework and supplicant to avoid skipping the EAP flow
243             // only when TOFU is in use.
244             clearNativeData();
245             Log.d(TAG, "Remove native cached data and networks for TOFU.");
246         }
247     }
248 
249     /**
250      * Do necessary clean up on stopping client mode.
251      */
cleanup()252     public void cleanup() {
253         dismissDialogAndNotification();
254         unregisterCertificateNotificationReceiver();
255         clearInternalData();
256         mWifiConfigManager.removeOnNetworkUpdateListener(mOnNetworkUpdateListener);
257     }
258 
259     /**
260      * Stores a received certificate for later use.
261      *
262      * @param ssid the target network SSID.
263      * @param depth the depth of this cert. The Root CA should be 0 or
264      *        a positive number, and the server cert is 0.
265      * @param certInfo a certificate info object from the server.
266      * @return true if the cert is cached; otherwise, false.
267      */
addPendingCertificate(@onNull String ssid, int depth, @NonNull CertificateEventInfo certInfo)268     public boolean addPendingCertificate(@NonNull String ssid, int depth,
269             @NonNull CertificateEventInfo certInfo) {
270         String configProfileKey = mCurrentTofuConfig != null
271                 ? mCurrentTofuConfig.getProfileKey() : "null";
272         if (TextUtils.isEmpty(ssid)) return false;
273         if (null == mCurrentTofuConfig) return false;
274         if (!TextUtils.equals(ssid, mCurrentTofuConfig.SSID)) return false;
275         if (null == certInfo) return false;
276         if (depth < 0) return false;
277 
278         // If TOFU is not supported return immediately, although this should not happen since
279         // the caller code flow is only active when TOFU is supported.
280         if (!mIsTrustOnFirstUseSupported) return false;
281 
282         // If insecure configurations are allowed and this configuration is configured with
283         // "Do not validate" (i.e. TOFU is disabled), skip loading the certificates (no need for
284         // them anyway) and don't disconnect the network.
285         if (mIsInsecureEnterpriseConfigurationAllowed
286                 && !mCurrentTofuConfig.enterpriseConfig.isTrustOnFirstUseEnabled()) {
287             Log.d(TAG, "Certificates are not required for this connection");
288             return false;
289         }
290 
291         if (depth == 0) {
292             // Disable network selection upon receiving the server certificate
293             putNetworkOnHold();
294         }
295 
296         if (!mServerCertChain.contains(certInfo.getCert())) {
297             mServerCertChain.addFirst(certInfo.getCert());
298             Log.d(TAG, "addPendingCertificate: " + "SSID=" + ssid + " depth=" + depth
299                     + " certHash=" + certInfo.getCertHash() + " current config=" + configProfileKey
300                     + "\ncertificate content:\n" + certInfo.getCert());
301         }
302 
303         // 0 is the tail, i.e. the server cert.
304         if (depth == 0 && null == mPendingServerCert) {
305             mPendingServerCert = certInfo.getCert();
306             mPendingServerCertSubjectInfo = CertificateSubjectInfo.parse(
307                     certInfo.getCert().getSubjectX500Principal().getName());
308             if (null == mPendingServerCertSubjectInfo) {
309                 Log.e(TAG, "Cert has no valid subject.");
310                 return false;
311             }
312             mPendingServerCertIssuerInfo = CertificateSubjectInfo.parse(
313                     certInfo.getCert().getIssuerX500Principal().getName());
314             if (null == mPendingServerCertIssuerInfo) {
315                 Log.e(TAG, "Cert has no valid issuer.");
316                 return false;
317             }
318             mServerCertHash = certInfo.getCertHash();
319         }
320 
321         // Root or intermediate cert.
322         if (depth < mPendingRootCaCertDepth) {
323             return true;
324         }
325         mPendingRootCaCertDepth = depth;
326         mPendingRootCaCert = certInfo.getCert();
327 
328         return true;
329     }
330 
331     /**
332      * Ask for the user approval if necessary.
333      *
334      * For TOFU is supported and an EAP network without a CA certificate.
335      * - if insecure EAP networks are not allowed
336      *    - if TOFU is not enabled, disconnect it.
337      *    - if no pending CA cert, disconnect it.
338      *    - if no server cert, disconnect it.
339      * - if insecure EAP networks are allowed and TOFU is not enabled
340      *    - follow no TOFU support flow.
341      * - if TOFU is enabled, CA cert is pending, and server cert is pending
342      *     - gate the connecitvity event here
343      *     - if this request is from a user, launch a dialog to get the user approval.
344      *     - if this request is from auto-connect, launch a notification.
345      * If TOFU is not supported, the confirmation flow is similar. Instead of installing CA
346      * cert from the server, just mark this network is approved by the user.
347      *
348      * @param isUserSelected indicates that this connection is triggered by a user.
349      * @return true if user approval dialog is displayed and the network is pending.
350      */
startUserApprovalIfNecessary(boolean isUserSelected)351     public boolean startUserApprovalIfNecessary(boolean isUserSelected) {
352         if (null == mConnectingConfig || null == mCurrentTofuConfig) return false;
353         if (mConnectingConfig.networkId != mCurrentTofuConfig.networkId) return false;
354 
355         // If Trust On First Use is supported and insecure enterprise configuration
356         // is not allowed, TOFU must be used for an Enterprise network without certs. This should
357         // not happen because the TOFU flag will be set during boot if these conditions are met.
358         if (mIsTrustOnFirstUseSupported && !mIsInsecureEnterpriseConfigurationAllowed
359                 && !mCurrentTofuConfig.enterpriseConfig.isTrustOnFirstUseEnabled()) {
360             Log.e(TAG, "Upgrade insecure connection to TOFU.");
361             mCurrentTofuConfig.enterpriseConfig.enableTrustOnFirstUse(true);
362         }
363 
364         if (useTrustOnFirstUse()) {
365             if (null == mPendingRootCaCert) {
366                 Log.e(TAG, "No valid CA cert for TLS-based connection.");
367                 handleError(mCurrentTofuConfig.SSID);
368                 return false;
369             }
370             if (null == mPendingServerCert) {
371                 Log.e(TAG, "No valid Server cert for TLS-based connection.");
372                 handleError(mCurrentTofuConfig.SSID);
373                 return false;
374             }
375 
376             Log.d(TAG, "TOFU certificate chain:");
377             for (X509Certificate cert : mServerCertChain) {
378                 Log.d(TAG, cert.getSubjectX500Principal().getName());
379             }
380 
381             if (!configureServerValidationMethod()) {
382                 Log.e(TAG, "Server cert chain is invalid.");
383                 String ssid = mCurrentTofuConfig.SSID;
384                 handleError(ssid);
385                 createCertificateErrorNotification(isUserSelected, ssid);
386                 return false;
387             }
388         } else if (mIsInsecureEnterpriseConfigurationAllowed) {
389             Log.i(TAG, "Insecure networks without a Root CA cert are allowed.");
390             return false;
391         }
392 
393         if (isUserSelected) {
394             askForUserApprovalForCaCertificate();
395         } else {
396             notifyUserForCaCertificate();
397         }
398         return true;
399     }
400 
401     /**
402      * Create a notification or a dialog when a server certificate is invalid
403      */
createCertificateErrorNotification(boolean isUserSelected, String ssid)404     private void createCertificateErrorNotification(boolean isUserSelected, String ssid) {
405         String title = mContext.getString(R.string.wifi_tofu_invalid_cert_chain_title, ssid);
406         String message = mContext.getString(R.string.wifi_tofu_invalid_cert_chain_message);
407         String okButtonText = mContext.getString(
408                 R.string.wifi_tofu_invalid_cert_chain_ok_text);
409 
410         if (TextUtils.isEmpty(title) || TextUtils.isEmpty(message)) return;
411 
412         if (isUserSelected) {
413             mTofuAlertDialog = mWifiDialogManager.createLegacySimpleDialog(
414                     title,
415                     message,
416                     null /* positiveButtonText */,
417                     null /* negativeButtonText */,
418                     okButtonText,
419                     new WifiDialogManager.SimpleDialogCallback() {
420                         @Override
421                         public void onPositiveButtonClicked() {
422                             // Not used.
423                         }
424 
425                         @Override
426                         public void onNegativeButtonClicked() {
427                             // Not used.
428                         }
429 
430                         @Override
431                         public void onNeutralButtonClicked() {
432                             // Not used.
433                         }
434 
435                         @Override
436                         public void onCancelled() {
437                             // Not used.
438                         }
439                     },
440                     new WifiThreadRunner(mHandler));
441             mTofuAlertDialog.launchDialog();
442         } else {
443             Notification.Builder builder = mFacade.makeNotificationBuilder(mContext,
444                             WifiService.NOTIFICATION_NETWORK_ALERTS)
445                     .setSmallIcon(
446                             Icon.createWithResource(mContext.getWifiOverlayApkPkgName(),
447                                     com.android.wifi.resources.R
448                                             .drawable.stat_notify_wifi_in_range))
449                     .setContentTitle(title)
450                     .setContentText(message)
451                     .setStyle(new Notification.BigTextStyle().bigText(message))
452                     .setColor(mContext.getResources().getColor(
453                             android.R.color.system_notification_accent_color));
454             mNotificationManager.notify(SystemMessage.NOTE_SERVER_CA_CERTIFICATE,
455                     builder.build());
456         }
457     }
458 
459     /**
460      * Disable network selection, disconnect if necessary, and clear PMK cache
461      */
putNetworkOnHold()462     private void putNetworkOnHold() {
463         // Disable network selection upon receiving the server certificate
464         mWifiConfigManager.updateNetworkSelectionStatus(mCurrentTofuConfig.networkId,
465                 WifiConfiguration.NetworkSelectionStatus
466                         .DISABLED_BY_WIFI_MANAGER);
467 
468         // Force disconnect and clear PMK cache to avoid supplicant reconnection
469         mWifiNative.disconnect(mInterfaceName);
470         clearNativeData();
471     }
472 
473     /**
474      * Configure the server validation method based on the incoming server certificate chain.
475      * If a valid method is found, the method returns true, and the caller can continue the TOFU
476      * process.
477      *
478      * A valid method could be one of the following:
479      * 1. If only the leaf or a partial chain is provided, use server certificate pinning.
480      * 2. If a full chain is provided, use the provided Root CA, but only if we are able to
481      *    cryptographically validate it.
482      *
483      * If no certificates were received, or the certificates are invalid, or chain verification
484      * fails, the method returns false and the caller should abort the TOFU process.
485      */
configureServerValidationMethod()486     private boolean configureServerValidationMethod() {
487         if (mServerCertChain.size() == 0) {
488             Log.e(TAG, "No certificate chain provided by the server.");
489             return false;
490         }
491         if (mServerCertChain.size() == 1) {
492             Log.i(TAG, "Only one certificate provided, use server certificate pinning");
493             return true;
494         }
495         if (mPendingRootCaCert.getSubjectX500Principal().getName()
496                 .equals(mPendingRootCaCert.getIssuerX500Principal().getName())) {
497             if (mPendingRootCaCert.getVersion() >= 2
498                     && mPendingRootCaCert.getBasicConstraints() < 0) {
499                 Log.i(TAG, "Root CA with no CA bit set in basic constraints, "
500                         + "use server certificate pinning");
501                 return true;
502             }
503         } else {
504             Log.i(TAG, "Root CA is not self-signed, use server certificate pinning");
505             return true;
506         }
507 
508         CertPath certPath;
509         try {
510             certPath = CertificateFactory.getInstance("X.509").generateCertPath(mServerCertChain);
511         } catch (CertificateException e) {
512             Log.e(TAG, "Certificate chain is invalid.");
513             return false;
514         } catch (IllegalStateException e) {
515             Log.wtf(TAG, "Fail: " + e);
516             return false;
517         }
518         CertPathValidator certPathValidator;
519         try {
520             certPathValidator = CertPathValidator.getInstance("PKIX");
521         } catch (NoSuchAlgorithmException e) {
522             Log.wtf(TAG, "PKIX algorithm not supported.");
523             return false;
524         }
525         try {
526             Set<TrustAnchor> anchorSet = Set.of(new TrustAnchor(mPendingRootCaCert, null));
527             PKIXParameters params = new PKIXParameters(anchorSet);
528             params.setRevocationEnabled(false);
529             certPathValidator.validate(certPath, params);
530         } catch (InvalidAlgorithmParameterException e) {
531             Log.wtf(TAG, "Invalid algorithm exception.");
532             return false;
533         } catch (CertPathValidatorException e) {
534             Log.e(TAG, "Server certificate chain validation failed: " + e);
535             return false;
536         }
537 
538         // Validation succeeded, no need for the server cert hash
539         mServerCertHash = null;
540 
541         // Check if the Root CA certificate is in the trust store so that we could configure the
542         // connection to use the system store instead of an explicit Root CA.
543         mUseTrustStore = false;
544         if (mWifiNative.isSupplicantAidlServiceVersionAtLeast(2)) {
545             if (isCertInTrustStore(mPendingRootCaCert)) {
546                 mUseTrustStore = true;
547             }
548         }
549         Log.i(TAG, "Server certificate chain validation succeeded, use "
550                 + (mUseTrustStore ? "trust store" : "Root CA"));
551         return true;
552     }
553 
useTrustOnFirstUse()554     private boolean useTrustOnFirstUse() {
555         return mIsTrustOnFirstUseSupported
556                 && mCurrentTofuConfig.enterpriseConfig.isTrustOnFirstUseEnabled();
557     }
558 
registerCertificateNotificationReceiver()559     private void registerCertificateNotificationReceiver() {
560         unregisterCertificateNotificationReceiver();
561 
562         IntentFilter filter = new IntentFilter();
563         if (useTrustOnFirstUse()) {
564             filter.addAction(ACTION_CERT_NOTIF_TAP);
565         } else {
566             filter.addAction(ACTION_CERT_NOTIF_ACCEPT);
567             filter.addAction(ACTION_CERT_NOTIF_REJECT);
568         }
569         mContext.registerReceiver(mCertNotificationReceiver, filter, null, mHandler);
570         mIsCertNotificationReceiverRegistered = true;
571     }
572 
unregisterCertificateNotificationReceiver()573     private void unregisterCertificateNotificationReceiver() {
574         if (!mIsCertNotificationReceiverRegistered) return;
575 
576         mContext.unregisterReceiver(mCertNotificationReceiver);
577         mIsCertNotificationReceiverRegistered = false;
578     }
579 
580     @VisibleForTesting
handleAccept(@onNull String ssid)581     void handleAccept(@NonNull String ssid) {
582         if (!isConnectionValid(ssid)) return;
583 
584         if (!useTrustOnFirstUse()) {
585             mWifiConfigManager.setUserApproveNoCaCert(mCurrentTofuConfig.networkId, true);
586         } else {
587             if (null == mPendingRootCaCert || null == mPendingServerCert) {
588                 handleError(ssid);
589                 return;
590             }
591             if (!mWifiConfigManager.updateCaCertificate(
592                     mCurrentTofuConfig.networkId, mPendingRootCaCert, mPendingServerCert,
593                     mServerCertHash, mUseTrustStore)) {
594                 // The user approved this network,
595                 // keep the connection regardless of the result.
596                 Log.e(TAG, "Cannot update CA cert to network " + mCurrentTofuConfig.getProfileKey()
597                         + ", CA cert = " + mPendingRootCaCert);
598             }
599         }
600         int networkId = mCurrentTofuConfig.networkId;
601         mWifiConfigManager.updateNetworkSelectionStatus(networkId,
602                 WifiConfiguration.NetworkSelectionStatus.DISABLED_NONE);
603         dismissDialogAndNotification();
604         clearInternalData();
605 
606         if (null != mCallbacks) mCallbacks.onAccept(ssid, networkId);
607     }
608 
609     @VisibleForTesting
handleReject(@onNull String ssid)610     void handleReject(@NonNull String ssid) {
611         if (!isConnectionValid(ssid)) return;
612         boolean disconnectRequired = !useTrustOnFirstUse();
613 
614         mWifiConfigManager.updateNetworkSelectionStatus(mCurrentTofuConfig.networkId,
615                 WifiConfiguration.NetworkSelectionStatus.DISABLED_BY_WIFI_MANAGER);
616         dismissDialogAndNotification();
617         clearInternalData();
618         if (disconnectRequired) clearNativeData();
619         if (null != mCallbacks) mCallbacks.onReject(ssid, disconnectRequired);
620     }
621 
handleError(@ullable String ssid)622     private void handleError(@Nullable String ssid) {
623         if (mCurrentTofuConfig != null) {
624             mWifiConfigManager.updateNetworkSelectionStatus(mCurrentTofuConfig.networkId,
625                     WifiConfiguration.NetworkSelectionStatus
626                     .DISABLED_BY_WIFI_MANAGER);
627         }
628         dismissDialogAndNotification();
629         clearInternalData();
630         clearNativeData();
631 
632         if (null != mCallbacks) mCallbacks.onError(ssid);
633     }
634 
askForUserApprovalForCaCertificate()635     private void askForUserApprovalForCaCertificate() {
636         if (mCurrentTofuConfig == null || TextUtils.isEmpty(mCurrentTofuConfig.SSID)) return;
637         if (useTrustOnFirstUse()) {
638             if (null == mPendingRootCaCert || null == mPendingServerCert) {
639                 Log.e(TAG, "Cannot launch a dialog for TOFU without "
640                         + "a valid pending CA certificate.");
641                 return;
642             }
643         }
644         dismissDialogAndNotification();
645 
646         String title = useTrustOnFirstUse()
647                 ? mContext.getString(R.string.wifi_ca_cert_dialog_title)
648                 : mContext.getString(R.string.wifi_ca_cert_dialog_preT_title);
649         String positiveButtonText = useTrustOnFirstUse()
650                 ? mContext.getString(R.string.wifi_ca_cert_dialog_continue_text)
651                 : mContext.getString(R.string.wifi_ca_cert_dialog_preT_continue_text);
652         String negativeButtonText = useTrustOnFirstUse()
653                 ? mContext.getString(R.string.wifi_ca_cert_dialog_abort_text)
654                 : mContext.getString(R.string.wifi_ca_cert_dialog_preT_abort_text);
655 
656         String message;
657         String messageUrl = null;
658         int messageUrlStart = 0;
659         int messageUrlEnd = 0;
660         if (useTrustOnFirstUse()) {
661             StringBuilder contentBuilder = new StringBuilder()
662                     .append(mContext.getString(R.string.wifi_ca_cert_dialog_message_hint))
663                     .append(mContext.getString(
664                             R.string.wifi_ca_cert_dialog_message_server_name_text,
665                             mPendingServerCertSubjectInfo.commonName))
666                     .append(mContext.getString(
667                             R.string.wifi_ca_cert_dialog_message_issuer_name_text,
668                             mPendingServerCertIssuerInfo.commonName));
669             if (!TextUtils.isEmpty(mPendingServerCertSubjectInfo.organization)) {
670                 contentBuilder.append(mContext.getString(
671                         R.string.wifi_ca_cert_dialog_message_organization_text,
672                         mPendingServerCertSubjectInfo.organization));
673             }
674             final Date expiration = mPendingServerCert.getNotAfter();
675             if (expiration != null) {
676                 contentBuilder.append(mContext.getString(
677                         R.string.wifi_ca_cert_dialog_message_expiration_text,
678                         DateFormat.getMediumDateFormat(mContext).format(expiration)));
679             }
680             final String fingerprint = getDigest(mPendingServerCert, "SHA256");
681             if (!TextUtils.isEmpty(fingerprint)) {
682                 contentBuilder.append(mContext.getString(
683                         R.string.wifi_ca_cert_dialog_message_signature_name_text, fingerprint));
684             }
685             message = contentBuilder.toString();
686         } else {
687             String hint = mContext.getString(
688                     R.string.wifi_ca_cert_dialog_preT_message_hint, mCurrentTofuConfig.SSID);
689             String linkText = mContext.getString(
690                     R.string.wifi_ca_cert_dialog_preT_message_link);
691             message = hint + " " + linkText;
692             messageUrl = mCaCertHelpLink;
693             messageUrlStart = hint.length() + 1;
694             messageUrlEnd = message.length();
695         }
696         mTofuAlertDialog = mWifiDialogManager.createLegacySimpleDialogWithUrl(
697                 title,
698                 message,
699                 messageUrl,
700                 messageUrlStart,
701                 messageUrlEnd,
702                 positiveButtonText,
703                 negativeButtonText,
704                 null /* neutralButtonText */,
705                 new WifiDialogManager.SimpleDialogCallback() {
706                     @Override
707                     public void onPositiveButtonClicked() {
708                         if (mCurrentTofuConfig == null) {
709                             return;
710                         }
711                         Log.d(TAG, "User accepted the server certificate");
712                         handleAccept(mCurrentTofuConfig.SSID);
713                     }
714 
715                     @Override
716                     public void onNegativeButtonClicked() {
717                         if (mCurrentTofuConfig == null) {
718                             return;
719                         }
720                         Log.d(TAG, "User rejected the server certificate");
721                         handleReject(mCurrentTofuConfig.SSID);
722                     }
723 
724                     @Override
725                     public void onNeutralButtonClicked() {
726                         // Not used.
727                         if (mCurrentTofuConfig == null) {
728                             return;
729                         }
730                         Log.d(TAG, "User input neutral");
731                         handleReject(mCurrentTofuConfig.SSID);
732                     }
733 
734                     @Override
735                     public void onCancelled() {
736                         if (mCurrentTofuConfig == null) {
737                             return;
738                         }
739                         Log.d(TAG, "User input canceled");
740                         handleReject(mCurrentTofuConfig.SSID);
741                     }
742                 },
743                 new WifiThreadRunner(mHandler));
744         mTofuAlertDialog.launchDialog();
745     }
746 
genCaCertNotifIntent( @onNull String action, @NonNull String ssid)747     private PendingIntent genCaCertNotifIntent(
748             @NonNull String action, @NonNull String ssid) {
749         Intent intent = new Intent(action)
750                 .setPackage(mContext.getServiceWifiPackageName())
751                 .putExtra(EXTRA_PENDING_CERT_SSID, ssid);
752         return mFacade.getBroadcast(mContext, 0, intent,
753                 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
754     }
755 
notifyUserForCaCertificate()756     private void notifyUserForCaCertificate() {
757         if (mCurrentTofuConfig == null) return;
758         if (useTrustOnFirstUse()) {
759             if (null == mPendingRootCaCert) return;
760             if (null == mPendingServerCert) return;
761         }
762         dismissDialogAndNotification();
763 
764         PendingIntent tapPendingIntent;
765         if (useTrustOnFirstUse()) {
766             tapPendingIntent = genCaCertNotifIntent(ACTION_CERT_NOTIF_TAP, mCurrentTofuConfig.SSID);
767         } else {
768             Intent openLinkIntent = new Intent(Intent.ACTION_VIEW)
769                     .setData(Uri.parse(mCaCertHelpLink))
770                     .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
771             tapPendingIntent = mFacade.getActivity(mContext, 0, openLinkIntent,
772                     PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
773         }
774 
775         String title = useTrustOnFirstUse()
776                 ? mContext.getString(R.string.wifi_ca_cert_notification_title)
777                 : mContext.getString(R.string.wifi_ca_cert_notification_preT_title);
778         String content = useTrustOnFirstUse()
779                 ? mContext.getString(R.string.wifi_ca_cert_notification_message,
780                         mCurrentTofuConfig.SSID)
781                 : mContext.getString(R.string.wifi_ca_cert_notification_preT_message,
782                         mCurrentTofuConfig.SSID);
783         Notification.Builder builder = mFacade.makeNotificationBuilder(mContext,
784                 WifiService.NOTIFICATION_NETWORK_ALERTS)
785                 .setSmallIcon(Icon.createWithResource(mContext.getWifiOverlayApkPkgName(),
786                             com.android.wifi.resources.R.drawable.stat_notify_wifi_in_range))
787                 .setContentTitle(title)
788                 .setContentText(content)
789                 .setStyle(new Notification.BigTextStyle().bigText(content))
790                 .setContentIntent(tapPendingIntent)
791                 .setOngoing(true)
792                 .setColor(mContext.getResources().getColor(
793                             android.R.color.system_notification_accent_color));
794         // On a device which does not support Trust On First Use,
795         // a user can accept or reject this network via the notification.
796         if (!useTrustOnFirstUse()) {
797             Notification.Action acceptAction = new Notification.Action.Builder(
798                     null /* icon */,
799                     mContext.getString(R.string.wifi_ca_cert_dialog_preT_continue_text),
800                     genCaCertNotifIntent(ACTION_CERT_NOTIF_ACCEPT, mCurrentTofuConfig.SSID))
801                     .build();
802             Notification.Action rejectAction = new Notification.Action.Builder(
803                     null /* icon */,
804                     mContext.getString(R.string.wifi_ca_cert_dialog_preT_abort_text),
805                     genCaCertNotifIntent(ACTION_CERT_NOTIF_REJECT, mCurrentTofuConfig.SSID))
806                     .build();
807             builder.addAction(rejectAction).addAction(acceptAction);
808         }
809         mNotificationManager.notify(SystemMessage.NOTE_SERVER_CA_CERTIFICATE, builder.build());
810     }
811 
dismissDialogAndNotification()812     private void dismissDialogAndNotification() {
813         mNotificationManager.cancel(SystemMessage.NOTE_SERVER_CA_CERTIFICATE);
814         if (mTofuAlertDialog != null) {
815             mTofuAlertDialog.dismissDialog();
816             mTofuAlertDialog = null;
817         }
818     }
819 
clearInternalData()820     private void clearInternalData() {
821         mPendingRootCaCertDepth = -1;
822         mPendingRootCaCert = null;
823         mPendingServerCert = null;
824         mPendingServerCertSubjectInfo = null;
825         mPendingServerCertIssuerInfo = null;
826         mCurrentTofuConfig = null;
827         mServerCertHash = null;
828         mUseTrustStore = false;
829     }
830 
clearNativeData()831     private void clearNativeData() {
832         // PMK should be cleared or it would skip EAP flow next time.
833         if (null != mCurrentTofuConfig) {
834             mWifiNative.removeNetworkCachedData(mCurrentTofuConfig.networkId);
835         }
836         // remove network so that supplicant's PMKSA cache is cleared
837         mWifiNative.removeAllNetworks(mInterfaceName);
838     }
839 
840     // There might be two possible conditions that there is no
841     // valid information to handle this response:
842     // 1. A new network request is fired just before getting the response.
843     //    As a result, this response is invalid and should be ignored.
844     // 2. There is something wrong, and it stops at an abnormal state.
845     //    For this case, we should go back DisconnectedState to
846     //    recover the state machine.
847     // Unfortunatually, we cannot identify the condition without valid information.
848     // If condition #1 occurs, and we found that the target SSID is changed,
849     // it should transit to L3Connected soon normally, just ignore this message.
850     // If condition #2 occurs, clear existing data and notify the client mode
851     // via onError callback.
isConnectionValid(@ullable String ssid)852     private boolean isConnectionValid(@Nullable String ssid) {
853         if (TextUtils.isEmpty(ssid) || null == mCurrentTofuConfig) {
854             handleError(null);
855             return false;
856         }
857 
858         if (!TextUtils.equals(ssid, mCurrentTofuConfig.SSID)) {
859             Log.w(TAG, "Target SSID " + mCurrentTofuConfig.SSID
860                     + " is different from TOFU returned SSID" + ssid);
861             return false;
862         }
863         return true;
864     }
865 
866     @VisibleForTesting
getDigest(X509Certificate x509Certificate, String algorithm)867     static String getDigest(X509Certificate x509Certificate, String algorithm) {
868         if (x509Certificate == null) {
869             return "";
870         }
871         try {
872             byte[] bytes = x509Certificate.getEncoded();
873             MessageDigest md = MessageDigest.getInstance(algorithm);
874             byte[] digest = md.digest(bytes);
875             return fingerprint(digest);
876         } catch (CertificateEncodingException ignored) {
877             return "";
878         } catch (NoSuchAlgorithmException ignored) {
879             return "";
880         }
881     }
882 
fingerprint(byte[] bytes)883     private static String fingerprint(byte[] bytes) {
884         if (bytes == null) {
885             return "";
886         }
887         StringJoiner sj = new StringJoiner(":");
888         for (byte b : bytes) {
889             sj.add(HexDump.toHexString(b));
890         }
891         return sj.toString();
892     }
893 
894     /** The callbacks object to notify the consumer. */
895     public static class InsecureEapNetworkHandlerCallbacks {
896         /**
897          * When a certificate is accepted, this callback is called.
898          *
899          * @param ssid SSID of the network.
900          * @param networkId  network ID
901          */
onAccept(@onNull String ssid, int networkId)902         public void onAccept(@NonNull String ssid, int networkId) {}
903         /**
904          * When a certificate is rejected, this callback is called.
905          *
906          * @param ssid SSID of the network.
907          * @param disconnectRequired Set to true if the network is currently connected
908          */
onReject(@onNull String ssid, boolean disconnectRequired)909         public void onReject(@NonNull String ssid, boolean disconnectRequired) {}
910         /**
911          * When there are no valid data to handle this insecure EAP network,
912          * this callback is called.
913          *
914          * @param ssid SSID of the network, it might be null.
915          */
onError(@ullable String ssid)916         public void onError(@Nullable String ssid) {}
917     }
918 
919     /**
920      * Listener for config manager network config related events.
921      */
922     private class OnNetworkUpdateListener implements
923             WifiConfigManager.OnNetworkUpdateListener {
924         @Override
onNetworkRemoved(WifiConfiguration config)925         public void onNetworkRemoved(WifiConfiguration config) {
926             // Dismiss TOFU dialog if the network of the current Tofu config is removed.
927             if (config == null || mCurrentTofuConfig == null
928                     || mTofuAlertDialog == null
929                     || config.networkId != mCurrentTofuConfig.networkId) return;
930 
931             dismissDialogAndNotification();
932         }
933     }
934 
935     /**
936      * Check if a given Root CA certificate exists in the Android trust store
937      *
938      * @param rootCaCert the Root CA certificate to check
939      * @return true if the Root CA certificate is found in the trust store, false otherwise
940      */
isCertInTrustStore(X509Certificate rootCaCert)941     private boolean isCertInTrustStore(X509Certificate rootCaCert) {
942         try {
943             // Get the Android trust store.
944             KeyStore keystore = KeyStore.getInstance("AndroidCAStore");
945             keystore.load(null);
946 
947             Enumeration<String> aliases = keystore.aliases();
948             while (aliases.hasMoreElements()) {
949                 String alias = aliases.nextElement();
950                 X509Certificate trusted = (X509Certificate) keystore.getCertificate(alias);
951                 if (trusted.getSubjectDN().equals(rootCaCert.getSubjectDN())) {
952                     // Check that the supplied cert was actually signed by the key we trust.
953                     rootCaCert.verify(trusted.getPublicKey());
954                     return true;
955                 }
956             }
957         } catch (Exception e) {
958             // Fall through
959             Log.e(TAG, e.getMessage());
960         }
961         // The certificate is not in the trust store.
962         return false;
963     }
964 }
965