PasspointProvider.java revision 8c22a001ebe4537fe5c0da1112e256dbd9d55ae7
1/* 2 * Copyright (C) 2016 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 17package com.android.server.wifi.hotspot2; 18 19import android.net.wifi.EAPConstants; 20import android.net.wifi.WifiConfiguration; 21import android.net.wifi.WifiEnterpriseConfig; 22import android.net.wifi.hotspot2.PasspointConfiguration; 23import android.net.wifi.hotspot2.pps.Credential; 24import android.net.wifi.hotspot2.pps.Credential.SimCredential; 25import android.net.wifi.hotspot2.pps.Credential.UserCredential; 26import android.security.Credentials; 27import android.text.TextUtils; 28import android.util.Base64; 29import android.util.Log; 30 31import com.android.server.wifi.IMSIParameter; 32import com.android.server.wifi.SIMAccessor; 33import com.android.server.wifi.WifiKeyStore; 34import com.android.server.wifi.hotspot2.anqp.ANQPElement; 35import com.android.server.wifi.hotspot2.anqp.Constants.ANQPElementType; 36import com.android.server.wifi.hotspot2.anqp.DomainNameElement; 37import com.android.server.wifi.hotspot2.anqp.NAIRealmElement; 38import com.android.server.wifi.hotspot2.anqp.RoamingConsortiumElement; 39import com.android.server.wifi.hotspot2.anqp.ThreeGPPNetworkElement; 40import com.android.server.wifi.hotspot2.anqp.eap.AuthParam; 41import com.android.server.wifi.hotspot2.anqp.eap.NonEAPInnerAuth; 42 43import java.nio.charset.StandardCharsets; 44import java.security.MessageDigest; 45import java.security.NoSuchAlgorithmException; 46import java.security.cert.CertificateEncodingException; 47import java.security.cert.X509Certificate; 48import java.util.Arrays; 49import java.util.List; 50import java.util.Map; 51import java.util.Objects; 52 53/** 54 * Abstraction for Passpoint service provider. This class contains the both static 55 * Passpoint configuration data and the runtime data (e.g. blacklisted SSIDs, statistics). 56 */ 57public class PasspointProvider { 58 private static final String TAG = "PasspointProvider"; 59 60 /** 61 * Used as part of alias string for certificates and keys. The alias string is in the format 62 * of: [KEY_TYPE]_HS2_[ProviderID] 63 * For example: "CACERT_HS2_0", "USRCERT_HS2_0", "USRPKEY_HS2_0" 64 */ 65 private static final String ALIAS_HS_TYPE = "HS2_"; 66 67 private final PasspointConfiguration mConfig; 68 private final WifiKeyStore mKeyStore; 69 70 /** 71 * Aliases for the private keys and certificates installed in the keystore. Each alias 72 * is a suffix of the actual certificate or key name installed in the keystore. The 73 * certificate or key name in the keystore is consist of |Type|_|alias|. 74 * This will be consistent with the usage of the term "alias" in {@link WifiEnterpriseConfig}. 75 */ 76 private String mCaCertificateAlias; 77 private String mClientPrivateKeyAlias; 78 private String mClientCertificateAlias; 79 80 private final long mProviderId; 81 82 private final IMSIParameter mImsiParameter; 83 private final List<String> mMatchingSIMImsiList; 84 85 private final int mEAPMethodID; 86 private final AuthParam mAuthParam; 87 88 public PasspointProvider(PasspointConfiguration config, WifiKeyStore keyStore, 89 SIMAccessor simAccessor, long providerId) { 90 this(config, keyStore, simAccessor, providerId, null, null, null); 91 } 92 93 public PasspointProvider(PasspointConfiguration config, WifiKeyStore keyStore, 94 SIMAccessor simAccessor, long providerId, String caCertificateAlias, 95 String clientCertificateAlias, String clientPrivateKeyAlias) { 96 // Maintain a copy of the configuration to avoid it being updated by others. 97 mConfig = new PasspointConfiguration(config); 98 mKeyStore = keyStore; 99 mProviderId = providerId; 100 mCaCertificateAlias = caCertificateAlias; 101 mClientCertificateAlias = clientCertificateAlias; 102 mClientPrivateKeyAlias = clientPrivateKeyAlias; 103 104 // Setup EAP method and authentication parameter based on the credential. 105 if (mConfig.getCredential().getUserCredential() != null) { 106 mEAPMethodID = EAPConstants.EAP_TTLS; 107 mAuthParam = new NonEAPInnerAuth(NonEAPInnerAuth.getAuthTypeID( 108 mConfig.getCredential().getUserCredential().getNonEapInnerMethod())); 109 mImsiParameter = null; 110 mMatchingSIMImsiList = null; 111 } else if (mConfig.getCredential().getCertCredential() != null) { 112 mEAPMethodID = EAPConstants.EAP_TLS; 113 mAuthParam = null; 114 mImsiParameter = null; 115 mMatchingSIMImsiList = null; 116 } else { 117 mEAPMethodID = mConfig.getCredential().getSimCredential().getEapType(); 118 mAuthParam = null; 119 mImsiParameter = IMSIParameter.build( 120 mConfig.getCredential().getSimCredential().getImsi()); 121 mMatchingSIMImsiList = simAccessor.getMatchingImsis(mImsiParameter); 122 } 123 } 124 125 public PasspointConfiguration getConfig() { 126 // Return a copy of the configuration to avoid it being updated by others. 127 return new PasspointConfiguration(mConfig); 128 } 129 130 public String getCaCertificateAlias() { 131 return mCaCertificateAlias; 132 } 133 134 public String getClientPrivateKeyAlias() { 135 return mClientPrivateKeyAlias; 136 } 137 138 public String getClientCertificateAlias() { 139 return mClientCertificateAlias; 140 } 141 142 public long getProviderId() { 143 return mProviderId; 144 } 145 146 /** 147 * Install certificates and key based on current configuration. 148 * Note: the certificates and keys in the configuration will get cleared once 149 * they're installed in the keystore. 150 * 151 * @return true on success 152 */ 153 public boolean installCertsAndKeys() { 154 // Install CA certificate. 155 if (mConfig.getCredential().getCaCertificate() != null) { 156 String certName = Credentials.CA_CERTIFICATE + ALIAS_HS_TYPE + mProviderId; 157 if (!mKeyStore.putCertInKeyStore(certName, 158 mConfig.getCredential().getCaCertificate())) { 159 Log.e(TAG, "Failed to install CA Certificate"); 160 uninstallCertsAndKeys(); 161 return false; 162 } 163 mCaCertificateAlias = ALIAS_HS_TYPE + mProviderId; 164 } 165 166 // Install the client private key. 167 if (mConfig.getCredential().getClientPrivateKey() != null) { 168 String keyName = Credentials.USER_PRIVATE_KEY + ALIAS_HS_TYPE + mProviderId; 169 if (!mKeyStore.putKeyInKeyStore(keyName, 170 mConfig.getCredential().getClientPrivateKey())) { 171 Log.e(TAG, "Failed to install client private key"); 172 uninstallCertsAndKeys(); 173 return false; 174 } 175 mClientPrivateKeyAlias = ALIAS_HS_TYPE + mProviderId; 176 } 177 178 // Install the client certificate. 179 if (mConfig.getCredential().getClientCertificateChain() != null) { 180 X509Certificate clientCert = getClientCertificate( 181 mConfig.getCredential().getClientCertificateChain(), 182 mConfig.getCredential().getCertCredential().getCertSha256Fingerprint()); 183 if (clientCert == null) { 184 Log.e(TAG, "Failed to locate client certificate"); 185 uninstallCertsAndKeys(); 186 return false; 187 } 188 String certName = Credentials.USER_CERTIFICATE + ALIAS_HS_TYPE + mProviderId; 189 if (!mKeyStore.putCertInKeyStore(certName, clientCert)) { 190 Log.e(TAG, "Failed to install client certificate"); 191 uninstallCertsAndKeys(); 192 return false; 193 } 194 mClientCertificateAlias = ALIAS_HS_TYPE + mProviderId; 195 } 196 197 // Clear the keys and certificates in the configuration. 198 mConfig.getCredential().setCaCertificate(null); 199 mConfig.getCredential().setClientPrivateKey(null); 200 mConfig.getCredential().setClientCertificateChain(null); 201 return true; 202 } 203 204 /** 205 * Remove any installed certificates and key. 206 */ 207 public void uninstallCertsAndKeys() { 208 if (mCaCertificateAlias != null) { 209 if (!mKeyStore.removeEntryFromKeyStore( 210 Credentials.CA_CERTIFICATE + mCaCertificateAlias)) { 211 Log.e(TAG, "Failed to remove entry: " + mCaCertificateAlias); 212 } 213 mCaCertificateAlias = null; 214 } 215 if (mClientPrivateKeyAlias != null) { 216 if (!mKeyStore.removeEntryFromKeyStore( 217 Credentials.USER_PRIVATE_KEY + mClientPrivateKeyAlias)) { 218 Log.e(TAG, "Failed to remove entry: " + mClientPrivateKeyAlias); 219 } 220 mClientPrivateKeyAlias = null; 221 } 222 if (mClientCertificateAlias != null) { 223 if (!mKeyStore.removeEntryFromKeyStore( 224 Credentials.USER_CERTIFICATE + mClientCertificateAlias)) { 225 Log.e(TAG, "Failed to remove entry: " + mClientCertificateAlias); 226 } 227 mClientCertificateAlias = null; 228 } 229 } 230 231 /** 232 * Return the matching status with the given AP, based on the ANQP elements from the AP. 233 * 234 * @param anqpElements ANQP elements from the AP 235 * @return {@link PasspointMatch} 236 */ 237 public PasspointMatch match(Map<ANQPElementType, ANQPElement> anqpElements) { 238 PasspointMatch providerMatch = matchProvider(anqpElements); 239 240 // Perform authentication match against the NAI Realm. 241 int authMatch = ANQPMatcher.matchNAIRealm( 242 (NAIRealmElement) anqpElements.get(ANQPElementType.ANQPNAIRealm), 243 mConfig.getCredential().getRealm(), mEAPMethodID, mAuthParam); 244 245 // Auth mismatch, demote provider match. 246 if (authMatch == AuthMatch.NONE) { 247 return PasspointMatch.None; 248 } 249 250 // No realm match, return provider match as is. 251 if ((authMatch & AuthMatch.REALM) == 0) { 252 return providerMatch; 253 } 254 255 // Realm match, promote provider match to roaming if no other provider match is found. 256 return providerMatch == PasspointMatch.None ? PasspointMatch.RoamingProvider 257 : providerMatch; 258 } 259 260 /** 261 * Generate a WifiConfiguration based on the provider's configuration. The generated 262 * WifiConfiguration will include all the necessary credentials for network connection except 263 * the SSID, which should be added by the caller when the config is being used for network 264 * connection. 265 * 266 * @return {@link WifiConfiguration} 267 */ 268 public WifiConfiguration getWifiConfig() { 269 WifiConfiguration wifiConfig = new WifiConfiguration(); 270 wifiConfig.FQDN = mConfig.getHomeSp().getFqdn(); 271 if (mConfig.getHomeSp().getRoamingConsortiumOis() != null) { 272 wifiConfig.roamingConsortiumIds = Arrays.copyOf( 273 mConfig.getHomeSp().getRoamingConsortiumOis(), 274 mConfig.getHomeSp().getRoamingConsortiumOis().length); 275 } 276 wifiConfig.providerFriendlyName = mConfig.getHomeSp().getFriendlyName(); 277 wifiConfig.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_EAP); 278 wifiConfig.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.IEEE8021X); 279 280 WifiEnterpriseConfig enterpriseConfig = new WifiEnterpriseConfig(); 281 enterpriseConfig.setRealm(mConfig.getCredential().getRealm()); 282 if (mConfig.getCredential().getUserCredential() != null) { 283 buildEnterpriseConfigForUserCredential(enterpriseConfig, 284 mConfig.getCredential().getUserCredential()); 285 setAnonymousIdentityToNaiRealm(enterpriseConfig, mConfig.getCredential().getRealm()); 286 } else if (mConfig.getCredential().getCertCredential() != null) { 287 buildEnterpriseConfigForCertCredential(enterpriseConfig); 288 setAnonymousIdentityToNaiRealm(enterpriseConfig, mConfig.getCredential().getRealm()); 289 } else { 290 buildEnterpriseConfigForSimCredential(enterpriseConfig, 291 mConfig.getCredential().getSimCredential()); 292 } 293 wifiConfig.enterpriseConfig = enterpriseConfig; 294 return wifiConfig; 295 } 296 297 @Override 298 public boolean equals(Object thatObject) { 299 if (this == thatObject) { 300 return true; 301 } 302 if (!(thatObject instanceof PasspointProvider)) { 303 return false; 304 } 305 PasspointProvider that = (PasspointProvider) thatObject; 306 return mProviderId == that.mProviderId 307 && TextUtils.equals(mCaCertificateAlias, that.mCaCertificateAlias) 308 && TextUtils.equals(mClientCertificateAlias, that.mClientCertificateAlias) 309 && TextUtils.equals(mClientPrivateKeyAlias, that.mClientPrivateKeyAlias) 310 && (mConfig == null ? that.mConfig == null : mConfig.equals(that.mConfig)); 311 } 312 313 @Override 314 public int hashCode() { 315 return Objects.hash(mProviderId, mCaCertificateAlias, mClientCertificateAlias, 316 mClientPrivateKeyAlias, mConfig); 317 } 318 319 /** 320 * Retrieve the client certificate from the certificates chain. The certificate 321 * with the matching SHA256 digest is the client certificate. 322 * 323 * @param certChain The client certificates chain 324 * @param expectedSha256Fingerprint The expected SHA256 digest of the client certificate 325 * @return {@link java.security.cert.X509Certificate} 326 */ 327 private static X509Certificate getClientCertificate(X509Certificate[] certChain, 328 byte[] expectedSha256Fingerprint) { 329 if (certChain == null) { 330 return null; 331 } 332 try { 333 MessageDigest digester = MessageDigest.getInstance("SHA-256"); 334 for (X509Certificate certificate : certChain) { 335 digester.reset(); 336 byte[] fingerprint = digester.digest(certificate.getEncoded()); 337 if (Arrays.equals(expectedSha256Fingerprint, fingerprint)) { 338 return certificate; 339 } 340 } 341 } catch (CertificateEncodingException | NoSuchAlgorithmException e) { 342 return null; 343 } 344 345 return null; 346 } 347 348 /** 349 * Perform a provider match based on the given ANQP elements. 350 * 351 * @param anqpElements List of ANQP elements 352 * @return {@link PasspointMatch} 353 */ 354 private PasspointMatch matchProvider(Map<ANQPElementType, ANQPElement> anqpElements) { 355 // Domain name matching. 356 if (ANQPMatcher.matchDomainName( 357 (DomainNameElement) anqpElements.get(ANQPElementType.ANQPDomName), 358 mConfig.getHomeSp().getFqdn(), mImsiParameter, mMatchingSIMImsiList)) { 359 return PasspointMatch.HomeProvider; 360 } 361 362 // Roaming Consortium OI matching. 363 if (ANQPMatcher.matchRoamingConsortium( 364 (RoamingConsortiumElement) anqpElements.get(ANQPElementType.ANQPRoamingConsortium), 365 mConfig.getHomeSp().getRoamingConsortiumOis())) { 366 return PasspointMatch.RoamingProvider; 367 } 368 369 // 3GPP Network matching. 370 if (ANQPMatcher.matchThreeGPPNetwork( 371 (ThreeGPPNetworkElement) anqpElements.get(ANQPElementType.ANQP3GPPNetwork), 372 mImsiParameter, mMatchingSIMImsiList)) { 373 return PasspointMatch.RoamingProvider; 374 } 375 return PasspointMatch.None; 376 } 377 378 /** 379 * Fill in WifiEnterpriseConfig with information from an user credential. 380 * 381 * @param config Instance of {@link WifiEnterpriseConfig} 382 * @param credential Instance of {@link UserCredential} 383 */ 384 private void buildEnterpriseConfigForUserCredential(WifiEnterpriseConfig config, 385 Credential.UserCredential credential) { 386 byte[] pwOctets = Base64.decode(credential.getPassword(), Base64.DEFAULT); 387 String decodedPassword = new String(pwOctets, StandardCharsets.UTF_8); 388 config.setEapMethod(WifiEnterpriseConfig.Eap.TTLS); 389 config.setIdentity(credential.getUsername()); 390 config.setPassword(decodedPassword); 391 config.setCaCertificateAlias(mCaCertificateAlias); 392 int phase2Method = WifiEnterpriseConfig.Phase2.NONE; 393 switch (credential.getNonEapInnerMethod()) { 394 case "PAP": 395 phase2Method = WifiEnterpriseConfig.Phase2.PAP; 396 break; 397 case "MS-CHAP": 398 phase2Method = WifiEnterpriseConfig.Phase2.MSCHAP; 399 break; 400 case "MS-CHAP-V2": 401 phase2Method = WifiEnterpriseConfig.Phase2.MSCHAPV2; 402 break; 403 default: 404 // Should never happen since this is already validated when the provider is 405 // added. 406 Log.wtf(TAG, "Unsupported Auth: " + credential.getNonEapInnerMethod()); 407 break; 408 } 409 config.setPhase2Method(phase2Method); 410 } 411 412 /** 413 * Fill in WifiEnterpriseConfig with information from a certificate credential. 414 * 415 * @param config Instance of {@link WifiEnterpriseConfig} 416 */ 417 private void buildEnterpriseConfigForCertCredential(WifiEnterpriseConfig config) { 418 config.setEapMethod(WifiEnterpriseConfig.Eap.TLS); 419 config.setClientCertificateAlias(mClientCertificateAlias); 420 config.setCaCertificateAlias(mCaCertificateAlias); 421 } 422 423 /** 424 * Fill in WifiEnterpriseConfig with information from a SIM credential. 425 * 426 * @param config Instance of {@link WifiEnterpriseConfig} 427 * @param credential Instance of {@link SimCredential} 428 */ 429 private void buildEnterpriseConfigForSimCredential(WifiEnterpriseConfig config, 430 Credential.SimCredential credential) { 431 int eapMethod = WifiEnterpriseConfig.Eap.NONE; 432 switch(credential.getEapType()) { 433 case EAPConstants.EAP_SIM: 434 eapMethod = WifiEnterpriseConfig.Eap.SIM; 435 break; 436 case EAPConstants.EAP_AKA: 437 eapMethod = WifiEnterpriseConfig.Eap.AKA; 438 break; 439 case EAPConstants.EAP_AKA_PRIME: 440 eapMethod = WifiEnterpriseConfig.Eap.AKA_PRIME; 441 break; 442 default: 443 // Should never happen since this is already validated when the provider is 444 // added. 445 Log.wtf(TAG, "Unsupported EAP Method: " + credential.getEapType()); 446 break; 447 } 448 config.setEapMethod(eapMethod); 449 config.setPlmn(credential.getImsi()); 450 } 451 452 private static void setAnonymousIdentityToNaiRealm(WifiEnterpriseConfig config, String realm) { 453 /** 454 * Set WPA supplicant's anonymous identity field to a string containing the NAI realm, so 455 * that this value will be sent to the EAP server as part of the EAP-Response/ Identity 456 * packet. WPA supplicant will reset this field after using it for the EAP-Response/Identity 457 * packet, and revert to using the (real) identity field for subsequent transactions that 458 * request an identity (e.g. in EAP-TTLS). 459 * 460 * This NAI realm value (the portion of the identity after the '@') is used to tell the 461 * AAA server which AAA/H to forward packets to. The hardcoded username, "anonymous", is a 462 * placeholder that is not used--it is set to this value by convention. See Section 5.1 of 463 * RFC3748 for more details. 464 * 465 * NOTE: we do not set this value for EAP-SIM/AKA/AKA', since the EAP server expects the 466 * EAP-Response/Identity packet to contain an actual, IMSI-based identity, in order to 467 * identify the device. 468 */ 469 config.setAnonymousIdentity("anonymous@" + realm); 470 } 471} 472