PasspointProvider.java revision a7f0f6e6fc75c94ab9af3cf1a6f390390c640d70
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 // Aliases for the private keys and certificates installed in the keystore. 71 private String mCaCertificateAlias; 72 private String mClientPrivateKeyAlias; 73 private String mClientCertificateAlias; 74 75 /** 76 * The suffix of the alias using for storing certificates and keys. Each alias is prefix 77 * with the key or certificate type. In key/certificate installation, the full alias is 78 * used. However, the setCaCertificateAlias and setClientCertificateAlias function 79 * in WifiEnterpriseConfig, the alias that it is referring to is actually the suffix, since 80 * WifiEnterpriseConfig will append the appropriate prefix to that alias based on the type. 81 */ 82 private final String mKeyStoreAliasSuffix; 83 private final long mProviderId; 84 85 private final IMSIParameter mImsiParameter; 86 private final List<String> mMatchingSIMImsiList; 87 88 private final int mEAPMethodID; 89 private final AuthParam mAuthParam; 90 91 public PasspointProvider(PasspointConfiguration config, WifiKeyStore keyStore, 92 SIMAccessor simAccessor, long providerId) { 93 this(config, keyStore, simAccessor, providerId, null, null, null); 94 } 95 96 public PasspointProvider(PasspointConfiguration config, WifiKeyStore keyStore, 97 SIMAccessor simAccessor, long providerId, String caCertificateAlias, 98 String clientCertificateAlias, String clientPrivateKeyAlias) { 99 // Maintain a copy of the configuration to avoid it being updated by others. 100 mConfig = new PasspointConfiguration(config); 101 mKeyStore = keyStore; 102 mKeyStoreAliasSuffix = ALIAS_HS_TYPE + providerId; 103 mProviderId = providerId; 104 mCaCertificateAlias = caCertificateAlias; 105 mClientCertificateAlias = clientCertificateAlias; 106 mClientPrivateKeyAlias = clientPrivateKeyAlias; 107 108 // Setup EAP method and authentication parameter based on the credential. 109 if (mConfig.getCredential().getUserCredential() != null) { 110 mEAPMethodID = EAPConstants.EAP_TTLS; 111 mAuthParam = new NonEAPInnerAuth(NonEAPInnerAuth.getAuthTypeID( 112 mConfig.getCredential().getUserCredential().getNonEapInnerMethod())); 113 mImsiParameter = null; 114 mMatchingSIMImsiList = null; 115 } else if (mConfig.getCredential().getCertCredential() != null) { 116 mEAPMethodID = EAPConstants.EAP_TLS; 117 mAuthParam = null; 118 mImsiParameter = null; 119 mMatchingSIMImsiList = null; 120 } else { 121 mEAPMethodID = mConfig.getCredential().getSimCredential().getEapType(); 122 mAuthParam = null; 123 mImsiParameter = IMSIParameter.build( 124 mConfig.getCredential().getSimCredential().getImsi()); 125 mMatchingSIMImsiList = simAccessor.getMatchingImsis(mImsiParameter); 126 } 127 } 128 129 public PasspointConfiguration getConfig() { 130 // Return a copy of the configuration to avoid it being updated by others. 131 return new PasspointConfiguration(mConfig); 132 } 133 134 public String getCaCertificateAlias() { 135 return mCaCertificateAlias; 136 } 137 138 public String getClientPrivateKeyAlias() { 139 return mClientPrivateKeyAlias; 140 } 141 142 public String getClientCertificateAlias() { 143 return mClientCertificateAlias; 144 } 145 146 public long getProviderId() { 147 return mProviderId; 148 } 149 150 /** 151 * Install certificates and key based on current configuration. 152 * Note: the certificates and keys in the configuration will get cleared once 153 * they're installed in the keystore. 154 * 155 * @return true on success 156 */ 157 public boolean installCertsAndKeys() { 158 // Install CA certificate. 159 if (mConfig.getCredential().getCaCertificate() != null) { 160 String alias = Credentials.CA_CERTIFICATE + mKeyStoreAliasSuffix; 161 if (!mKeyStore.putCertInKeyStore(alias, mConfig.getCredential().getCaCertificate())) { 162 Log.e(TAG, "Failed to install CA Certificate"); 163 uninstallCertsAndKeys(); 164 return false; 165 } 166 mCaCertificateAlias = alias; 167 } 168 169 // Install the client private key. 170 if (mConfig.getCredential().getClientPrivateKey() != null) { 171 String alias = Credentials.USER_PRIVATE_KEY + mKeyStoreAliasSuffix; 172 if (!mKeyStore.putKeyInKeyStore(alias, 173 mConfig.getCredential().getClientPrivateKey())) { 174 Log.e(TAG, "Failed to install client private key"); 175 uninstallCertsAndKeys(); 176 return false; 177 } 178 mClientPrivateKeyAlias = alias; 179 } 180 181 // Install the client certificate. 182 if (mConfig.getCredential().getClientCertificateChain() != null) { 183 X509Certificate clientCert = getClientCertificate( 184 mConfig.getCredential().getClientCertificateChain(), 185 mConfig.getCredential().getCertCredential().getCertSha256Fingerprint()); 186 if (clientCert == null) { 187 Log.e(TAG, "Failed to locate client certificate"); 188 uninstallCertsAndKeys(); 189 return false; 190 } 191 String alias = Credentials.USER_CERTIFICATE + mKeyStoreAliasSuffix; 192 if (!mKeyStore.putCertInKeyStore(alias, clientCert)) { 193 Log.e(TAG, "Failed to install client certificate"); 194 uninstallCertsAndKeys(); 195 return false; 196 } 197 mClientCertificateAlias = alias; 198 } 199 200 // Clear the keys and certificates in the configuration. 201 mConfig.getCredential().setCaCertificate(null); 202 mConfig.getCredential().setClientPrivateKey(null); 203 mConfig.getCredential().setClientCertificateChain(null); 204 return true; 205 } 206 207 /** 208 * Remove any installed certificates and key. 209 */ 210 public void uninstallCertsAndKeys() { 211 if (mCaCertificateAlias != null) { 212 if (!mKeyStore.removeEntryFromKeyStore(mCaCertificateAlias)) { 213 Log.e(TAG, "Failed to remove entry: " + mCaCertificateAlias); 214 } 215 mCaCertificateAlias = null; 216 } 217 if (mClientPrivateKeyAlias != null) { 218 if (!mKeyStore.removeEntryFromKeyStore(mClientPrivateKeyAlias)) { 219 Log.e(TAG, "Failed to remove entry: " + mClientPrivateKeyAlias); 220 } 221 mClientPrivateKeyAlias = null; 222 } 223 if (mClientCertificateAlias != null) { 224 if (!mKeyStore.removeEntryFromKeyStore(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(mKeyStoreAliasSuffix); 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(mKeyStoreAliasSuffix); 420 config.setCaCertificateAlias(mKeyStoreAliasSuffix); 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