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