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