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 networkId networkId of the target network.
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(int networkId, int depth, @NonNull CertificateEventInfo certInfo)268     public boolean addPendingCertificate(int networkId, int depth,
269             @NonNull CertificateEventInfo certInfo) {
270         String configProfileKey = mCurrentTofuConfig != null
271                 ? mCurrentTofuConfig.getProfileKey() : "null";
272         if (networkId == WifiConfiguration.INVALID_NETWORK_ID) {
273             return false;
274         }
275         if (null == mCurrentTofuConfig) return false;
276         if (mCurrentTofuConfig.networkId != networkId) {
277             return false;
278         }
279         if (null == certInfo) return false;
280         if (depth < 0) return false;
281 
282         // If TOFU is not supported return immediately, although this should not happen since
283         // the caller code flow is only active when TOFU is supported.
284         if (!mIsTrustOnFirstUseSupported) return false;
285 
286         // If insecure configurations are allowed and this configuration is configured with
287         // "Do not validate" (i.e. TOFU is disabled), skip loading the certificates (no need for
288         // them anyway) and don't disconnect the network.
289         if (mIsInsecureEnterpriseConfigurationAllowed
290                 && !mCurrentTofuConfig.enterpriseConfig.isTrustOnFirstUseEnabled()) {
291             Log.d(TAG, "Certificates are not required for this connection");
292             return false;
293         }
294 
295         if (depth == 0) {
296             // Disable network selection upon receiving the server certificate
297             putNetworkOnHold();
298         }
299 
300         if (!mServerCertChain.contains(certInfo.getCert())) {
301             mServerCertChain.addFirst(certInfo.getCert());
302             Log.d(TAG, "addPendingCertificate: " + "SSID=" + mCurrentTofuConfig.SSID
303                     + " depth=" + depth + " certHash=" + certInfo.getCertHash()
304                     + " current config=" + configProfileKey
305                     + "\ncertificate content:\n" + certInfo.getCert());
306         }
307 
308         // 0 is the tail, i.e. the server cert.
309         if (depth == 0 && null == mPendingServerCert) {
310             mPendingServerCert = certInfo.getCert();
311             mPendingServerCertSubjectInfo = CertificateSubjectInfo.parse(
312                     certInfo.getCert().getSubjectX500Principal().getName());
313             if (null == mPendingServerCertSubjectInfo) {
314                 Log.e(TAG, "Cert has no valid subject.");
315                 return false;
316             }
317             mPendingServerCertIssuerInfo = CertificateSubjectInfo.parse(
318                     certInfo.getCert().getIssuerX500Principal().getName());
319             if (null == mPendingServerCertIssuerInfo) {
320                 Log.e(TAG, "Cert has no valid issuer.");
321                 return false;
322             }
323             mServerCertHash = certInfo.getCertHash();
324         }
325 
326         // Root or intermediate cert.
327         if (depth < mPendingRootCaCertDepth) {
328             return true;
329         }
330         mPendingRootCaCertDepth = depth;
331         mPendingRootCaCert = certInfo.getCert();
332 
333         return true;
334     }
335 
336     /**
337      * Ask for the user approval if necessary.
338      *
339      * For TOFU is supported and an EAP network without a CA certificate.
340      * - if insecure EAP networks are not allowed
341      *    - if TOFU is not enabled, disconnect it.
342      *    - if no pending CA cert, disconnect it.
343      *    - if no server cert, disconnect it.
344      * - if insecure EAP networks are allowed and TOFU is not enabled
345      *    - follow no TOFU support flow.
346      * - if TOFU is enabled, CA cert is pending, and server cert is pending
347      *     - gate the connecitvity event here
348      *     - if this request is from a user, launch a dialog to get the user approval.
349      *     - if this request is from auto-connect, launch a notification.
350      * If TOFU is not supported, the confirmation flow is similar. Instead of installing CA
351      * cert from the server, just mark this network is approved by the user.
352      *
353      * @param isUserSelected indicates that this connection is triggered by a user.
354      * @return true if user approval dialog is displayed and the network is pending.
355      */
startUserApprovalIfNecessary(boolean isUserSelected)356     public boolean startUserApprovalIfNecessary(boolean isUserSelected) {
357         if (null == mConnectingConfig || null == mCurrentTofuConfig) return false;
358         if (mConnectingConfig.networkId != mCurrentTofuConfig.networkId) return false;
359 
360         // If Trust On First Use is supported and insecure enterprise configuration
361         // is not allowed, TOFU must be used for an Enterprise network without certs. This should
362         // not happen because the TOFU flag will be set during boot if these conditions are met.
363         if (mIsTrustOnFirstUseSupported && !mIsInsecureEnterpriseConfigurationAllowed
364                 && !mCurrentTofuConfig.enterpriseConfig.isTrustOnFirstUseEnabled()) {
365             Log.e(TAG, "Upgrade insecure connection to TOFU.");
366             mCurrentTofuConfig.enterpriseConfig.enableTrustOnFirstUse(true);
367         }
368 
369         if (useTrustOnFirstUse()) {
370             if (null == mPendingRootCaCert) {
371                 Log.e(TAG, "No valid CA cert for TLS-based connection.");
372                 handleError(mCurrentTofuConfig.SSID);
373                 return false;
374             }
375             if (null == mPendingServerCert) {
376                 Log.e(TAG, "No valid Server cert for TLS-based connection.");
377                 handleError(mCurrentTofuConfig.SSID);
378                 return false;
379             }
380 
381             Log.d(TAG, "TOFU certificate chain:");
382             for (X509Certificate cert : mServerCertChain) {
383                 Log.d(TAG, cert.getSubjectX500Principal().getName());
384             }
385 
386             if (null == mPendingServerCertSubjectInfo) {
387                 handleError(mCurrentTofuConfig.SSID);
388                 Log.d(TAG, "No valid subject info in Server cert for TLS-based connection.");
389                 return false;
390             }
391 
392             if (null == mPendingServerCertIssuerInfo) {
393                 handleError(mCurrentTofuConfig.SSID);
394                 Log.d(TAG, "No valid issuer info in Server cert for TLS-based connection.");
395                 return false;
396             }
397 
398             if (!configureServerValidationMethod()) {
399                 Log.e(TAG, "Server cert chain is invalid.");
400                 String ssid = mCurrentTofuConfig.SSID;
401                 handleError(ssid);
402                 createCertificateErrorNotification(isUserSelected, ssid);
403                 return false;
404             }
405         } else if (mIsInsecureEnterpriseConfigurationAllowed) {
406             Log.i(TAG, "Insecure networks without a Root CA cert are allowed.");
407             return false;
408         }
409 
410         if (isUserSelected) {
411             askForUserApprovalForCaCertificate();
412         } else {
413             notifyUserForCaCertificate();
414         }
415         return true;
416     }
417 
418     /**
419      * Create a notification or a dialog when a server certificate is invalid
420      */
createCertificateErrorNotification(boolean isUserSelected, String ssid)421     private void createCertificateErrorNotification(boolean isUserSelected, String ssid) {
422         String title = mContext.getString(R.string.wifi_tofu_invalid_cert_chain_title, ssid);
423         String message = mContext.getString(R.string.wifi_tofu_invalid_cert_chain_message);
424         String okButtonText = mContext.getString(
425                 R.string.wifi_tofu_invalid_cert_chain_ok_text);
426 
427         if (TextUtils.isEmpty(title) || TextUtils.isEmpty(message)) return;
428 
429         if (isUserSelected) {
430             mTofuAlertDialog = mWifiDialogManager.createLegacySimpleDialog(
431                     title,
432                     message,
433                     null /* positiveButtonText */,
434                     null /* negativeButtonText */,
435                     okButtonText,
436                     new WifiDialogManager.SimpleDialogCallback() {
437                         @Override
438                         public void onPositiveButtonClicked() {
439                             // Not used.
440                         }
441 
442                         @Override
443                         public void onNegativeButtonClicked() {
444                             // Not used.
445                         }
446 
447                         @Override
448                         public void onNeutralButtonClicked() {
449                             // Not used.
450                         }
451 
452                         @Override
453                         public void onCancelled() {
454                             // Not used.
455                         }
456                     },
457                     new WifiThreadRunner(mHandler));
458             mTofuAlertDialog.launchDialog();
459         } else {
460             Notification.Builder builder = mFacade.makeNotificationBuilder(mContext,
461                             WifiService.NOTIFICATION_NETWORK_ALERTS)
462                     .setSmallIcon(
463                             Icon.createWithResource(mContext.getWifiOverlayApkPkgName(),
464                                     com.android.wifi.resources.R
465                                             .drawable.stat_notify_wifi_in_range))
466                     .setContentTitle(title)
467                     .setContentText(message)
468                     .setStyle(new Notification.BigTextStyle().bigText(message))
469                     .setColor(mContext.getResources().getColor(
470                             android.R.color.system_notification_accent_color));
471             mNotificationManager.notify(SystemMessage.NOTE_SERVER_CA_CERTIFICATE,
472                     builder.build());
473         }
474     }
475 
476     /**
477      * Disable network selection, disconnect if necessary, and clear PMK cache
478      */
putNetworkOnHold()479     private void putNetworkOnHold() {
480         // Disable network selection upon receiving the server certificate
481         mWifiConfigManager.updateNetworkSelectionStatus(mCurrentTofuConfig.networkId,
482                 WifiConfiguration.NetworkSelectionStatus
483                         .DISABLED_BY_WIFI_MANAGER);
484 
485         // Force disconnect and clear PMK cache to avoid supplicant reconnection
486         mWifiNative.disconnect(mInterfaceName);
487         clearNativeData();
488     }
489 
490     /**
491      * Check whether certificate pinning should be used.
492      *
493      * @param verbose whether to print logs during the check.
494      * @return true if certificate pinning should be used, false otherwise.
495      */
useCertificatePinning(boolean verbose)496     private boolean useCertificatePinning(boolean verbose) {
497         if (mServerCertChain.size() == 1) {
498             if (verbose) {
499                 Log.i(TAG, "Only one certificate provided, use server certificate pinning");
500             }
501             return true;
502         }
503         if (mPendingRootCaCert.getSubjectX500Principal().getName()
504                 .equals(mPendingRootCaCert.getIssuerX500Principal().getName())) {
505             if (mPendingRootCaCert.getVersion() >= 2
506                     && mPendingRootCaCert.getBasicConstraints() < 0) {
507                 if (verbose) {
508                     Log.i(TAG, "Root CA with no CA bit set in basic constraints, "
509                             + "use server certificate pinning");
510                 }
511                 return true;
512             }
513         } else {
514             if (verbose) {
515                 Log.i(TAG, "Root CA is not self-signed, use server certificate pinning");
516             }
517             return true;
518         }
519         return false;
520     }
521 
522     /**
523      * Configure the server validation method based on the incoming server certificate chain.
524      * If a valid method is found, the method returns true, and the caller can continue the TOFU
525      * process.
526      *
527      * A valid method could be one of the following:
528      * 1. If only the leaf or a partial chain is provided, use server certificate pinning.
529      * 2. If a full chain is provided, use the provided Root CA, but only if we are able to
530      *    cryptographically validate it.
531      *
532      * If no certificates were received, or the certificates are invalid, or chain verification
533      * fails, the method returns false and the caller should abort the TOFU process.
534      */
configureServerValidationMethod()535     private boolean configureServerValidationMethod() {
536         if (mServerCertChain.size() == 0) {
537             Log.e(TAG, "No certificate chain provided by the server.");
538             return false;
539         }
540         if (useCertificatePinning(true)) {
541             return true;
542         }
543 
544         CertPath certPath;
545         try {
546             certPath = CertificateFactory.getInstance("X.509").generateCertPath(mServerCertChain);
547         } catch (CertificateException e) {
548             Log.e(TAG, "Certificate chain is invalid.");
549             return false;
550         } catch (IllegalStateException e) {
551             Log.wtf(TAG, "Fail: " + e);
552             return false;
553         }
554         CertPathValidator certPathValidator;
555         try {
556             certPathValidator = CertPathValidator.getInstance("PKIX");
557         } catch (NoSuchAlgorithmException e) {
558             Log.wtf(TAG, "PKIX algorithm not supported.");
559             return false;
560         }
561         try {
562             Set<TrustAnchor> anchorSet = Set.of(new TrustAnchor(mPendingRootCaCert, null));
563             PKIXParameters params = new PKIXParameters(anchorSet);
564             params.setRevocationEnabled(false);
565             certPathValidator.validate(certPath, params);
566         } catch (InvalidAlgorithmParameterException e) {
567             Log.wtf(TAG, "Invalid algorithm exception.");
568             return false;
569         } catch (CertPathValidatorException e) {
570             Log.e(TAG, "Server certificate chain validation failed: " + e);
571             return false;
572         }
573 
574         // Validation succeeded, no need for the server cert hash
575         mServerCertHash = null;
576 
577         // Check if the Root CA certificate is in the trust store so that we could configure the
578         // connection to use the system store instead of an explicit Root CA.
579         mUseTrustStore = false;
580         if (mWifiNative.isSupplicantAidlServiceVersionAtLeast(2)) {
581             if (isCertInTrustStore(mPendingRootCaCert)) {
582                 mUseTrustStore = true;
583             }
584         }
585         Log.i(TAG, "Server certificate chain validation succeeded, use "
586                 + (mUseTrustStore ? "trust store" : "Root CA"));
587         return true;
588     }
589 
useTrustOnFirstUse()590     private boolean useTrustOnFirstUse() {
591         return mIsTrustOnFirstUseSupported
592                 && mCurrentTofuConfig.enterpriseConfig.isTrustOnFirstUseEnabled();
593     }
594 
registerCertificateNotificationReceiver()595     private void registerCertificateNotificationReceiver() {
596         unregisterCertificateNotificationReceiver();
597 
598         IntentFilter filter = new IntentFilter();
599         if (useTrustOnFirstUse()) {
600             filter.addAction(ACTION_CERT_NOTIF_TAP);
601         } else {
602             filter.addAction(ACTION_CERT_NOTIF_ACCEPT);
603             filter.addAction(ACTION_CERT_NOTIF_REJECT);
604         }
605         mContext.registerReceiver(mCertNotificationReceiver, filter, null, mHandler);
606         mIsCertNotificationReceiverRegistered = true;
607     }
608 
unregisterCertificateNotificationReceiver()609     private void unregisterCertificateNotificationReceiver() {
610         if (!mIsCertNotificationReceiverRegistered) return;
611 
612         mContext.unregisterReceiver(mCertNotificationReceiver);
613         mIsCertNotificationReceiverRegistered = false;
614     }
615 
616     @VisibleForTesting
handleAccept(@onNull String ssid)617     void handleAccept(@NonNull String ssid) {
618         if (!isConnectionValid(ssid)) return;
619 
620         if (!useTrustOnFirstUse()) {
621             mWifiConfigManager.setUserApproveNoCaCert(mCurrentTofuConfig.networkId, true);
622         } else {
623             if (null == mPendingRootCaCert || null == mPendingServerCert) {
624                 handleError(ssid);
625                 return;
626             }
627             if (!mWifiConfigManager.updateCaCertificate(
628                     mCurrentTofuConfig.networkId, mPendingRootCaCert, mPendingServerCert,
629                     mServerCertHash, mUseTrustStore)) {
630                 // The user approved this network,
631                 // keep the connection regardless of the result.
632                 Log.e(TAG, "Cannot update CA cert to network " + mCurrentTofuConfig.getProfileKey()
633                         + ", CA cert = " + mPendingRootCaCert);
634             }
635             int postConnectionMethod = useCertificatePinning(false)
636                     ? WifiEnterpriseConfig.TOFU_STATE_CERT_PINNING
637                     : WifiEnterpriseConfig.TOFU_STATE_CONFIGURE_ROOT_CA;
638             mWifiConfigManager.setTofuPostConnectionState(
639                     mCurrentTofuConfig.networkId, postConnectionMethod);
640         }
641         int networkId = mCurrentTofuConfig.networkId;
642         mWifiConfigManager.setTofuDialogApproved(networkId, true);
643         mWifiConfigManager.updateNetworkSelectionStatus(networkId,
644                 WifiConfiguration.NetworkSelectionStatus.DISABLED_NONE);
645         dismissDialogAndNotification();
646         clearInternalData();
647 
648         if (null != mCallbacks) mCallbacks.onAccept(ssid, networkId);
649     }
650 
651     @VisibleForTesting
handleReject(@onNull String ssid)652     void handleReject(@NonNull String ssid) {
653         if (!isConnectionValid(ssid)) return;
654         boolean disconnectRequired = !useTrustOnFirstUse();
655 
656         mWifiConfigManager.setTofuDialogApproved(mCurrentTofuConfig.networkId, false);
657         mWifiConfigManager.updateNetworkSelectionStatus(mCurrentTofuConfig.networkId,
658                 WifiConfiguration.NetworkSelectionStatus.DISABLED_BY_WIFI_MANAGER);
659         dismissDialogAndNotification();
660         clearInternalData();
661         if (disconnectRequired) clearNativeData();
662         if (null != mCallbacks) mCallbacks.onReject(ssid, disconnectRequired);
663     }
664 
handleError(@ullable String ssid)665     private void handleError(@Nullable String ssid) {
666         if (mCurrentTofuConfig != null) {
667             mWifiConfigManager.updateNetworkSelectionStatus(mCurrentTofuConfig.networkId,
668                     WifiConfiguration.NetworkSelectionStatus
669                     .DISABLED_BY_WIFI_MANAGER);
670         }
671         dismissDialogAndNotification();
672         clearInternalData();
673         clearNativeData();
674 
675         if (null != mCallbacks) mCallbacks.onError(ssid);
676     }
677 
askForUserApprovalForCaCertificate()678     private void askForUserApprovalForCaCertificate() {
679         if (mCurrentTofuConfig == null || TextUtils.isEmpty(mCurrentTofuConfig.SSID)) return;
680         if (useTrustOnFirstUse()) {
681             if (null == mPendingRootCaCert || null == mPendingServerCert) {
682                 Log.e(TAG, "Cannot launch a dialog for TOFU without "
683                         + "a valid pending CA certificate.");
684                 return;
685             }
686         }
687         dismissDialogAndNotification();
688 
689         String title = useTrustOnFirstUse()
690                 ? mContext.getString(R.string.wifi_ca_cert_dialog_title)
691                 : mContext.getString(R.string.wifi_ca_cert_dialog_preT_title);
692         String positiveButtonText = useTrustOnFirstUse()
693                 ? mContext.getString(R.string.wifi_ca_cert_dialog_continue_text)
694                 : mContext.getString(R.string.wifi_ca_cert_dialog_preT_continue_text);
695         String negativeButtonText = useTrustOnFirstUse()
696                 ? mContext.getString(R.string.wifi_ca_cert_dialog_abort_text)
697                 : mContext.getString(R.string.wifi_ca_cert_dialog_preT_abort_text);
698 
699         String message;
700         String messageUrl = null;
701         int messageUrlStart = 0;
702         int messageUrlEnd = 0;
703         if (useTrustOnFirstUse()) {
704             StringBuilder contentBuilder = new StringBuilder()
705                     .append(mContext.getString(R.string.wifi_ca_cert_dialog_message_hint))
706                     .append(mContext.getString(
707                             R.string.wifi_ca_cert_dialog_message_server_name_text,
708                             mPendingServerCertSubjectInfo.commonName))
709                     .append(mContext.getString(
710                             R.string.wifi_ca_cert_dialog_message_issuer_name_text,
711                             mPendingServerCertIssuerInfo.commonName));
712             if (!TextUtils.isEmpty(mPendingServerCertSubjectInfo.organization)) {
713                 contentBuilder.append(mContext.getString(
714                         R.string.wifi_ca_cert_dialog_message_organization_text,
715                         mPendingServerCertSubjectInfo.organization));
716             }
717             final Date expiration = mPendingServerCert.getNotAfter();
718             if (expiration != null) {
719                 contentBuilder.append(mContext.getString(
720                         R.string.wifi_ca_cert_dialog_message_expiration_text,
721                         DateFormat.getMediumDateFormat(mContext).format(expiration)));
722             }
723             final String fingerprint = getDigest(mPendingServerCert, "SHA256");
724             if (!TextUtils.isEmpty(fingerprint)) {
725                 contentBuilder.append(mContext.getString(
726                         R.string.wifi_ca_cert_dialog_message_signature_name_text, fingerprint));
727             }
728             message = contentBuilder.toString();
729         } else {
730             String hint = mContext.getString(
731                     R.string.wifi_ca_cert_dialog_preT_message_hint, mCurrentTofuConfig.SSID);
732             String linkText = mContext.getString(
733                     R.string.wifi_ca_cert_dialog_preT_message_link);
734             message = hint + " " + linkText;
735             messageUrl = mCaCertHelpLink;
736             messageUrlStart = hint.length() + 1;
737             messageUrlEnd = message.length();
738         }
739         mTofuAlertDialog = mWifiDialogManager.createLegacySimpleDialogWithUrl(
740                 title,
741                 message,
742                 messageUrl,
743                 messageUrlStart,
744                 messageUrlEnd,
745                 positiveButtonText,
746                 negativeButtonText,
747                 null /* neutralButtonText */,
748                 new WifiDialogManager.SimpleDialogCallback() {
749                     @Override
750                     public void onPositiveButtonClicked() {
751                         if (mCurrentTofuConfig == null) {
752                             return;
753                         }
754                         Log.d(TAG, "User accepted the server certificate");
755                         handleAccept(mCurrentTofuConfig.SSID);
756                     }
757 
758                     @Override
759                     public void onNegativeButtonClicked() {
760                         if (mCurrentTofuConfig == null) {
761                             return;
762                         }
763                         Log.d(TAG, "User rejected the server certificate");
764                         handleReject(mCurrentTofuConfig.SSID);
765                     }
766 
767                     @Override
768                     public void onNeutralButtonClicked() {
769                         // Not used.
770                         if (mCurrentTofuConfig == null) {
771                             return;
772                         }
773                         Log.d(TAG, "User input neutral");
774                         handleReject(mCurrentTofuConfig.SSID);
775                     }
776 
777                     @Override
778                     public void onCancelled() {
779                         if (mCurrentTofuConfig == null) {
780                             return;
781                         }
782                         Log.d(TAG, "User input canceled");
783                         handleReject(mCurrentTofuConfig.SSID);
784                     }
785                 },
786                 new WifiThreadRunner(mHandler));
787         mTofuAlertDialog.launchDialog();
788     }
789 
genCaCertNotifIntent( @onNull String action, @NonNull String ssid)790     private PendingIntent genCaCertNotifIntent(
791             @NonNull String action, @NonNull String ssid) {
792         Intent intent = new Intent(action)
793                 .setPackage(mContext.getServiceWifiPackageName())
794                 .putExtra(EXTRA_PENDING_CERT_SSID, ssid);
795         return mFacade.getBroadcast(mContext, 0, intent,
796                 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
797     }
798 
notifyUserForCaCertificate()799     private void notifyUserForCaCertificate() {
800         if (mCurrentTofuConfig == null) return;
801         if (useTrustOnFirstUse()) {
802             if (null == mPendingRootCaCert) return;
803             if (null == mPendingServerCert) return;
804         }
805         dismissDialogAndNotification();
806 
807         PendingIntent tapPendingIntent;
808         if (useTrustOnFirstUse()) {
809             tapPendingIntent = genCaCertNotifIntent(ACTION_CERT_NOTIF_TAP, mCurrentTofuConfig.SSID);
810         } else {
811             Intent openLinkIntent = new Intent(Intent.ACTION_VIEW)
812                     .setData(Uri.parse(mCaCertHelpLink))
813                     .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
814             tapPendingIntent = mFacade.getActivity(mContext, 0, openLinkIntent,
815                     PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
816         }
817 
818         String title = useTrustOnFirstUse()
819                 ? mContext.getString(R.string.wifi_ca_cert_notification_title)
820                 : mContext.getString(R.string.wifi_ca_cert_notification_preT_title);
821         String content = useTrustOnFirstUse()
822                 ? mContext.getString(R.string.wifi_ca_cert_notification_message,
823                         mCurrentTofuConfig.SSID)
824                 : mContext.getString(R.string.wifi_ca_cert_notification_preT_message,
825                         mCurrentTofuConfig.SSID);
826         Notification.Builder builder = mFacade.makeNotificationBuilder(mContext,
827                 WifiService.NOTIFICATION_NETWORK_ALERTS)
828                 .setSmallIcon(Icon.createWithResource(mContext.getWifiOverlayApkPkgName(),
829                             com.android.wifi.resources.R.drawable.stat_notify_wifi_in_range))
830                 .setContentTitle(title)
831                 .setContentText(content)
832                 .setStyle(new Notification.BigTextStyle().bigText(content))
833                 .setContentIntent(tapPendingIntent)
834                 .setOngoing(true)
835                 .setColor(mContext.getResources().getColor(
836                             android.R.color.system_notification_accent_color));
837         // On a device which does not support Trust On First Use,
838         // a user can accept or reject this network via the notification.
839         if (!useTrustOnFirstUse()) {
840             Notification.Action acceptAction = new Notification.Action.Builder(
841                     null /* icon */,
842                     mContext.getString(R.string.wifi_ca_cert_dialog_preT_continue_text),
843                     genCaCertNotifIntent(ACTION_CERT_NOTIF_ACCEPT, mCurrentTofuConfig.SSID))
844                     .build();
845             Notification.Action rejectAction = new Notification.Action.Builder(
846                     null /* icon */,
847                     mContext.getString(R.string.wifi_ca_cert_dialog_preT_abort_text),
848                     genCaCertNotifIntent(ACTION_CERT_NOTIF_REJECT, mCurrentTofuConfig.SSID))
849                     .build();
850             builder.addAction(rejectAction).addAction(acceptAction);
851         }
852         mNotificationManager.notify(SystemMessage.NOTE_SERVER_CA_CERTIFICATE, builder.build());
853     }
854 
dismissDialogAndNotification()855     private void dismissDialogAndNotification() {
856         mNotificationManager.cancel(SystemMessage.NOTE_SERVER_CA_CERTIFICATE);
857         if (mTofuAlertDialog != null) {
858             mTofuAlertDialog.dismissDialog();
859             mTofuAlertDialog = null;
860         }
861     }
862 
clearInternalData()863     private void clearInternalData() {
864         mPendingRootCaCertDepth = -1;
865         mPendingRootCaCert = null;
866         mPendingServerCert = null;
867         mPendingServerCertSubjectInfo = null;
868         mPendingServerCertIssuerInfo = null;
869         mCurrentTofuConfig = null;
870         mServerCertHash = null;
871         mUseTrustStore = false;
872     }
873 
clearNativeData()874     private void clearNativeData() {
875         // PMK should be cleared or it would skip EAP flow next time.
876         if (null != mCurrentTofuConfig) {
877             mWifiNative.removeNetworkCachedData(mCurrentTofuConfig.networkId);
878         }
879         // remove network so that supplicant's PMKSA cache is cleared
880         mWifiNative.removeAllNetworks(mInterfaceName);
881     }
882 
883     // There might be two possible conditions that there is no
884     // valid information to handle this response:
885     // 1. A new network request is fired just before getting the response.
886     //    As a result, this response is invalid and should be ignored.
887     // 2. There is something wrong, and it stops at an abnormal state.
888     //    For this case, we should go back DisconnectedState to
889     //    recover the state machine.
890     // Unfortunatually, we cannot identify the condition without valid information.
891     // If condition #1 occurs, and we found that the target SSID is changed,
892     // it should transit to L3Connected soon normally, just ignore this message.
893     // If condition #2 occurs, clear existing data and notify the client mode
894     // via onError callback.
isConnectionValid(@ullable String ssid)895     private boolean isConnectionValid(@Nullable String ssid) {
896         if (TextUtils.isEmpty(ssid) || null == mCurrentTofuConfig) {
897             handleError(null);
898             return false;
899         }
900 
901         if (!TextUtils.equals(ssid, mCurrentTofuConfig.SSID)) {
902             Log.w(TAG, "Target SSID " + mCurrentTofuConfig.SSID
903                     + " is different from TOFU returned SSID" + ssid);
904             return false;
905         }
906         return true;
907     }
908 
909     @VisibleForTesting
getDigest(X509Certificate x509Certificate, String algorithm)910     static String getDigest(X509Certificate x509Certificate, String algorithm) {
911         if (x509Certificate == null) {
912             return "";
913         }
914         try {
915             byte[] bytes = x509Certificate.getEncoded();
916             MessageDigest md = MessageDigest.getInstance(algorithm);
917             byte[] digest = md.digest(bytes);
918             return fingerprint(digest);
919         } catch (CertificateEncodingException ignored) {
920             return "";
921         } catch (NoSuchAlgorithmException ignored) {
922             return "";
923         }
924     }
925 
fingerprint(byte[] bytes)926     private static String fingerprint(byte[] bytes) {
927         if (bytes == null) {
928             return "";
929         }
930         StringJoiner sj = new StringJoiner(":");
931         for (byte b : bytes) {
932             sj.add(HexDump.toHexString(b));
933         }
934         return sj.toString();
935     }
936 
937     /** The callbacks object to notify the consumer. */
938     public static class InsecureEapNetworkHandlerCallbacks {
939         /**
940          * When a certificate is accepted, this callback is called.
941          *
942          * @param ssid SSID of the network.
943          * @param networkId  network ID
944          */
onAccept(@onNull String ssid, int networkId)945         public void onAccept(@NonNull String ssid, int networkId) {}
946         /**
947          * When a certificate is rejected, this callback is called.
948          *
949          * @param ssid SSID of the network.
950          * @param disconnectRequired Set to true if the network is currently connected
951          */
onReject(@onNull String ssid, boolean disconnectRequired)952         public void onReject(@NonNull String ssid, boolean disconnectRequired) {}
953         /**
954          * When there are no valid data to handle this insecure EAP network,
955          * this callback is called.
956          *
957          * @param ssid SSID of the network, it might be null.
958          */
onError(@ullable String ssid)959         public void onError(@Nullable String ssid) {}
960     }
961 
962     /**
963      * Listener for config manager network config related events.
964      */
965     private class OnNetworkUpdateListener implements
966             WifiConfigManager.OnNetworkUpdateListener {
967         @Override
onNetworkRemoved(WifiConfiguration config)968         public void onNetworkRemoved(WifiConfiguration config) {
969             // Dismiss TOFU dialog if the network of the current Tofu config is removed.
970             if (config == null || mCurrentTofuConfig == null
971                     || mTofuAlertDialog == null
972                     || config.networkId != mCurrentTofuConfig.networkId) return;
973 
974             dismissDialogAndNotification();
975         }
976     }
977 
978     /**
979      * Check if a given Root CA certificate exists in the Android trust store
980      *
981      * @param rootCaCert the Root CA certificate to check
982      * @return true if the Root CA certificate is found in the trust store, false otherwise
983      */
isCertInTrustStore(X509Certificate rootCaCert)984     private boolean isCertInTrustStore(X509Certificate rootCaCert) {
985         try {
986             // Get the Android trust store.
987             KeyStore keystore = KeyStore.getInstance("AndroidCAStore");
988             keystore.load(null);
989 
990             Enumeration<String> aliases = keystore.aliases();
991             while (aliases.hasMoreElements()) {
992                 String alias = aliases.nextElement();
993                 X509Certificate trusted = (X509Certificate) keystore.getCertificate(alias);
994                 if (trusted.getSubjectDN().equals(rootCaCert.getSubjectDN())) {
995                     // Check that the supplied cert was actually signed by the key we trust.
996                     rootCaCert.verify(trusted.getPublicKey());
997                     return true;
998                 }
999             }
1000         } catch (Exception e) {
1001             // Fall through
1002             Log.e(TAG, e.getMessage(), e);
1003         }
1004         // The certificate is not in the trust store.
1005         return false;
1006     }
1007 }
1008