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