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