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