/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.wifi.hotspot2; import android.net.wifi.EAPConstants; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiEnterpriseConfig; import android.net.wifi.hotspot2.PasspointConfiguration; import android.net.wifi.hotspot2.pps.Credential; import android.net.wifi.hotspot2.pps.Credential.SimCredential; import android.net.wifi.hotspot2.pps.Credential.UserCredential; import android.net.wifi.hotspot2.pps.HomeSp; import android.security.Credentials; import android.text.TextUtils; import android.util.Base64; import android.util.Log; import com.android.server.wifi.IMSIParameter; import com.android.server.wifi.SIMAccessor; import com.android.server.wifi.WifiKeyStore; import com.android.server.wifi.hotspot2.anqp.ANQPElement; import com.android.server.wifi.hotspot2.anqp.Constants.ANQPElementType; import com.android.server.wifi.hotspot2.anqp.DomainNameElement; import com.android.server.wifi.hotspot2.anqp.NAIRealmElement; import com.android.server.wifi.hotspot2.anqp.RoamingConsortiumElement; import com.android.server.wifi.hotspot2.anqp.ThreeGPPNetworkElement; import com.android.server.wifi.hotspot2.anqp.eap.AuthParam; import com.android.server.wifi.hotspot2.anqp.eap.NonEAPInnerAuth; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Objects; /** * Abstraction for Passpoint service provider. This class contains the both static * Passpoint configuration data and the runtime data (e.g. blacklisted SSIDs, statistics). */ public class PasspointProvider { private static final String TAG = "PasspointProvider"; /** * Used as part of alias string for certificates and keys. The alias string is in the format * of: [KEY_TYPE]_HS2_[ProviderID] * For example: "CACERT_HS2_0", "USRCERT_HS2_0", "USRPKEY_HS2_0" */ private static final String ALIAS_HS_TYPE = "HS2_"; private final PasspointConfiguration mConfig; private final WifiKeyStore mKeyStore; /** * Aliases for the private keys and certificates installed in the keystore. Each alias * is a suffix of the actual certificate or key name installed in the keystore. The * certificate or key name in the keystore is consist of |Type|_|alias|. * This will be consistent with the usage of the term "alias" in {@link WifiEnterpriseConfig}. */ private String mCaCertificateAlias; private String mClientPrivateKeyAlias; private String mClientCertificateAlias; private final long mProviderId; private final int mCreatorUid; private final IMSIParameter mImsiParameter; private final List mMatchingSIMImsiList; private final int mEAPMethodID; private final AuthParam mAuthParam; public PasspointProvider(PasspointConfiguration config, WifiKeyStore keyStore, SIMAccessor simAccessor, long providerId, int creatorUid) { this(config, keyStore, simAccessor, providerId, creatorUid, null, null, null); } public PasspointProvider(PasspointConfiguration config, WifiKeyStore keyStore, SIMAccessor simAccessor, long providerId, int creatorUid, String caCertificateAlias, String clientCertificateAlias, String clientPrivateKeyAlias) { // Maintain a copy of the configuration to avoid it being updated by others. mConfig = new PasspointConfiguration(config); mKeyStore = keyStore; mProviderId = providerId; mCreatorUid = creatorUid; mCaCertificateAlias = caCertificateAlias; mClientCertificateAlias = clientCertificateAlias; mClientPrivateKeyAlias = clientPrivateKeyAlias; // Setup EAP method and authentication parameter based on the credential. if (mConfig.getCredential().getUserCredential() != null) { mEAPMethodID = EAPConstants.EAP_TTLS; mAuthParam = new NonEAPInnerAuth(NonEAPInnerAuth.getAuthTypeID( mConfig.getCredential().getUserCredential().getNonEapInnerMethod())); mImsiParameter = null; mMatchingSIMImsiList = null; } else if (mConfig.getCredential().getCertCredential() != null) { mEAPMethodID = EAPConstants.EAP_TLS; mAuthParam = null; mImsiParameter = null; mMatchingSIMImsiList = null; } else { mEAPMethodID = mConfig.getCredential().getSimCredential().getEapType(); mAuthParam = null; mImsiParameter = IMSIParameter.build( mConfig.getCredential().getSimCredential().getImsi()); mMatchingSIMImsiList = simAccessor.getMatchingImsis(mImsiParameter); } } public PasspointConfiguration getConfig() { // Return a copy of the configuration to avoid it being updated by others. return new PasspointConfiguration(mConfig); } public String getCaCertificateAlias() { return mCaCertificateAlias; } public String getClientPrivateKeyAlias() { return mClientPrivateKeyAlias; } public String getClientCertificateAlias() { return mClientCertificateAlias; } public long getProviderId() { return mProviderId; } public int getCreatorUid() { return mCreatorUid; } /** * Install certificates and key based on current configuration. * Note: the certificates and keys in the configuration will get cleared once * they're installed in the keystore. * * @return true on success */ public boolean installCertsAndKeys() { // Install CA certificate. if (mConfig.getCredential().getCaCertificate() != null) { String certName = Credentials.CA_CERTIFICATE + ALIAS_HS_TYPE + mProviderId; if (!mKeyStore.putCertInKeyStore(certName, mConfig.getCredential().getCaCertificate())) { Log.e(TAG, "Failed to install CA Certificate"); uninstallCertsAndKeys(); return false; } mCaCertificateAlias = ALIAS_HS_TYPE + mProviderId; } // Install the client private key. if (mConfig.getCredential().getClientPrivateKey() != null) { String keyName = Credentials.USER_PRIVATE_KEY + ALIAS_HS_TYPE + mProviderId; if (!mKeyStore.putKeyInKeyStore(keyName, mConfig.getCredential().getClientPrivateKey())) { Log.e(TAG, "Failed to install client private key"); uninstallCertsAndKeys(); return false; } mClientPrivateKeyAlias = ALIAS_HS_TYPE + mProviderId; } // Install the client certificate. if (mConfig.getCredential().getClientCertificateChain() != null) { X509Certificate clientCert = getClientCertificate( mConfig.getCredential().getClientCertificateChain(), mConfig.getCredential().getCertCredential().getCertSha256Fingerprint()); if (clientCert == null) { Log.e(TAG, "Failed to locate client certificate"); uninstallCertsAndKeys(); return false; } String certName = Credentials.USER_CERTIFICATE + ALIAS_HS_TYPE + mProviderId; if (!mKeyStore.putCertInKeyStore(certName, clientCert)) { Log.e(TAG, "Failed to install client certificate"); uninstallCertsAndKeys(); return false; } mClientCertificateAlias = ALIAS_HS_TYPE + mProviderId; } // Clear the keys and certificates in the configuration. mConfig.getCredential().setCaCertificate(null); mConfig.getCredential().setClientPrivateKey(null); mConfig.getCredential().setClientCertificateChain(null); return true; } /** * Remove any installed certificates and key. */ public void uninstallCertsAndKeys() { if (mCaCertificateAlias != null) { if (!mKeyStore.removeEntryFromKeyStore( Credentials.CA_CERTIFICATE + mCaCertificateAlias)) { Log.e(TAG, "Failed to remove entry: " + mCaCertificateAlias); } mCaCertificateAlias = null; } if (mClientPrivateKeyAlias != null) { if (!mKeyStore.removeEntryFromKeyStore( Credentials.USER_PRIVATE_KEY + mClientPrivateKeyAlias)) { Log.e(TAG, "Failed to remove entry: " + mClientPrivateKeyAlias); } mClientPrivateKeyAlias = null; } if (mClientCertificateAlias != null) { if (!mKeyStore.removeEntryFromKeyStore( Credentials.USER_CERTIFICATE + mClientCertificateAlias)) { Log.e(TAG, "Failed to remove entry: " + mClientCertificateAlias); } mClientCertificateAlias = null; } } /** * Return the matching status with the given AP, based on the ANQP elements from the AP. * * @param anqpElements ANQP elements from the AP * @return {@link PasspointMatch} */ public PasspointMatch match(Map anqpElements) { PasspointMatch providerMatch = matchProvider(anqpElements); // Perform authentication match against the NAI Realm. int authMatch = ANQPMatcher.matchNAIRealm( (NAIRealmElement) anqpElements.get(ANQPElementType.ANQPNAIRealm), mConfig.getCredential().getRealm(), mEAPMethodID, mAuthParam); // Auth mismatch, demote provider match. if (authMatch == AuthMatch.NONE) { return PasspointMatch.None; } // No realm match, return provider match as is. if ((authMatch & AuthMatch.REALM) == 0) { return providerMatch; } // Realm match, promote provider match to roaming if no other provider match is found. return providerMatch == PasspointMatch.None ? PasspointMatch.RoamingProvider : providerMatch; } /** * Generate a WifiConfiguration based on the provider's configuration. The generated * WifiConfiguration will include all the necessary credentials for network connection except * the SSID, which should be added by the caller when the config is being used for network * connection. * * @return {@link WifiConfiguration} */ public WifiConfiguration getWifiConfig() { WifiConfiguration wifiConfig = new WifiConfiguration(); wifiConfig.FQDN = mConfig.getHomeSp().getFqdn(); if (mConfig.getHomeSp().getRoamingConsortiumOis() != null) { wifiConfig.roamingConsortiumIds = Arrays.copyOf( mConfig.getHomeSp().getRoamingConsortiumOis(), mConfig.getHomeSp().getRoamingConsortiumOis().length); } wifiConfig.providerFriendlyName = mConfig.getHomeSp().getFriendlyName(); wifiConfig.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_EAP); wifiConfig.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.IEEE8021X); WifiEnterpriseConfig enterpriseConfig = new WifiEnterpriseConfig(); enterpriseConfig.setRealm(mConfig.getCredential().getRealm()); if (mConfig.getCredential().getUserCredential() != null) { buildEnterpriseConfigForUserCredential(enterpriseConfig, mConfig.getCredential().getUserCredential()); setAnonymousIdentityToNaiRealm(enterpriseConfig, mConfig.getCredential().getRealm()); } else if (mConfig.getCredential().getCertCredential() != null) { buildEnterpriseConfigForCertCredential(enterpriseConfig); setAnonymousIdentityToNaiRealm(enterpriseConfig, mConfig.getCredential().getRealm()); } else { buildEnterpriseConfigForSimCredential(enterpriseConfig, mConfig.getCredential().getSimCredential()); } wifiConfig.enterpriseConfig = enterpriseConfig; return wifiConfig; } /** * Convert a legacy {@link WifiConfiguration} representation of a Passpoint configuration to * a {@link PasspointConfiguration}. This is used for migrating legacy Passpoint * configuration (release N and older). * * @param wifiConfig The {@link WifiConfiguration} to convert * @return {@link PasspointConfiguration} */ public static PasspointConfiguration convertFromWifiConfig(WifiConfiguration wifiConfig) { PasspointConfiguration passpointConfig = new PasspointConfiguration(); // Setup HomeSP. HomeSp homeSp = new HomeSp(); if (TextUtils.isEmpty(wifiConfig.FQDN)) { Log.e(TAG, "Missing FQDN"); return null; } homeSp.setFqdn(wifiConfig.FQDN); homeSp.setFriendlyName(wifiConfig.providerFriendlyName); if (wifiConfig.roamingConsortiumIds != null) { homeSp.setRoamingConsortiumOis(Arrays.copyOf( wifiConfig.roamingConsortiumIds, wifiConfig.roamingConsortiumIds.length)); } passpointConfig.setHomeSp(homeSp); // Setup Credential. Credential credential = new Credential(); credential.setRealm(wifiConfig.enterpriseConfig.getRealm()); switch (wifiConfig.enterpriseConfig.getEapMethod()) { case WifiEnterpriseConfig.Eap.TTLS: credential.setUserCredential(buildUserCredentialFromEnterpriseConfig( wifiConfig.enterpriseConfig)); break; case WifiEnterpriseConfig.Eap.TLS: Credential.CertificateCredential certCred = new Credential.CertificateCredential(); certCred.setCertType(Credential.CertificateCredential.CERT_TYPE_X509V3); credential.setCertCredential(certCred); break; case WifiEnterpriseConfig.Eap.SIM: credential.setSimCredential(buildSimCredentialFromEnterpriseConfig( EAPConstants.EAP_SIM, wifiConfig.enterpriseConfig)); break; case WifiEnterpriseConfig.Eap.AKA: credential.setSimCredential(buildSimCredentialFromEnterpriseConfig( EAPConstants.EAP_AKA, wifiConfig.enterpriseConfig)); break; case WifiEnterpriseConfig.Eap.AKA_PRIME: credential.setSimCredential(buildSimCredentialFromEnterpriseConfig( EAPConstants.EAP_AKA_PRIME, wifiConfig.enterpriseConfig)); break; default: Log.e(TAG, "Unsupport EAP method: " + wifiConfig.enterpriseConfig.getEapMethod()); return null; } if (credential.getUserCredential() == null && credential.getCertCredential() == null && credential.getSimCredential() == null) { Log.e(TAG, "Missing credential"); return null; } passpointConfig.setCredential(credential); return passpointConfig; } @Override public boolean equals(Object thatObject) { if (this == thatObject) { return true; } if (!(thatObject instanceof PasspointProvider)) { return false; } PasspointProvider that = (PasspointProvider) thatObject; return mProviderId == that.mProviderId && TextUtils.equals(mCaCertificateAlias, that.mCaCertificateAlias) && TextUtils.equals(mClientCertificateAlias, that.mClientCertificateAlias) && TextUtils.equals(mClientPrivateKeyAlias, that.mClientPrivateKeyAlias) && (mConfig == null ? that.mConfig == null : mConfig.equals(that.mConfig)); } @Override public int hashCode() { return Objects.hash(mProviderId, mCaCertificateAlias, mClientCertificateAlias, mClientPrivateKeyAlias, mConfig); } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append("ProviderId: ").append(mProviderId).append("\n"); builder.append("CreatorUID: ").append(mCreatorUid).append("\n"); builder.append("Configuration Begin ---\n"); builder.append(mConfig); builder.append("Configuration End ---\n"); return builder.toString(); } /** * Retrieve the client certificate from the certificates chain. The certificate * with the matching SHA256 digest is the client certificate. * * @param certChain The client certificates chain * @param expectedSha256Fingerprint The expected SHA256 digest of the client certificate * @return {@link java.security.cert.X509Certificate} */ private static X509Certificate getClientCertificate(X509Certificate[] certChain, byte[] expectedSha256Fingerprint) { if (certChain == null) { return null; } try { MessageDigest digester = MessageDigest.getInstance("SHA-256"); for (X509Certificate certificate : certChain) { digester.reset(); byte[] fingerprint = digester.digest(certificate.getEncoded()); if (Arrays.equals(expectedSha256Fingerprint, fingerprint)) { return certificate; } } } catch (CertificateEncodingException | NoSuchAlgorithmException e) { return null; } return null; } /** * Perform a provider match based on the given ANQP elements. * * @param anqpElements List of ANQP elements * @return {@link PasspointMatch} */ private PasspointMatch matchProvider(Map anqpElements) { // Domain name matching. if (ANQPMatcher.matchDomainName( (DomainNameElement) anqpElements.get(ANQPElementType.ANQPDomName), mConfig.getHomeSp().getFqdn(), mImsiParameter, mMatchingSIMImsiList)) { return PasspointMatch.HomeProvider; } // Roaming Consortium OI matching. if (ANQPMatcher.matchRoamingConsortium( (RoamingConsortiumElement) anqpElements.get(ANQPElementType.ANQPRoamingConsortium), mConfig.getHomeSp().getRoamingConsortiumOis())) { return PasspointMatch.RoamingProvider; } // 3GPP Network matching. if (ANQPMatcher.matchThreeGPPNetwork( (ThreeGPPNetworkElement) anqpElements.get(ANQPElementType.ANQP3GPPNetwork), mImsiParameter, mMatchingSIMImsiList)) { return PasspointMatch.RoamingProvider; } return PasspointMatch.None; } /** * Fill in WifiEnterpriseConfig with information from an user credential. * * @param config Instance of {@link WifiEnterpriseConfig} * @param credential Instance of {@link UserCredential} */ private void buildEnterpriseConfigForUserCredential(WifiEnterpriseConfig config, Credential.UserCredential credential) { byte[] pwOctets = Base64.decode(credential.getPassword(), Base64.DEFAULT); String decodedPassword = new String(pwOctets, StandardCharsets.UTF_8); config.setEapMethod(WifiEnterpriseConfig.Eap.TTLS); config.setIdentity(credential.getUsername()); config.setPassword(decodedPassword); config.setCaCertificateAlias(mCaCertificateAlias); int phase2Method = WifiEnterpriseConfig.Phase2.NONE; switch (credential.getNonEapInnerMethod()) { case Credential.UserCredential.AUTH_METHOD_PAP: phase2Method = WifiEnterpriseConfig.Phase2.PAP; break; case Credential.UserCredential.AUTH_METHOD_MSCHAP: phase2Method = WifiEnterpriseConfig.Phase2.MSCHAP; break; case Credential.UserCredential.AUTH_METHOD_MSCHAPV2: phase2Method = WifiEnterpriseConfig.Phase2.MSCHAPV2; break; default: // Should never happen since this is already validated when the provider is // added. Log.wtf(TAG, "Unsupported Auth: " + credential.getNonEapInnerMethod()); break; } config.setPhase2Method(phase2Method); } /** * Fill in WifiEnterpriseConfig with information from a certificate credential. * * @param config Instance of {@link WifiEnterpriseConfig} */ private void buildEnterpriseConfigForCertCredential(WifiEnterpriseConfig config) { config.setEapMethod(WifiEnterpriseConfig.Eap.TLS); config.setClientCertificateAlias(mClientCertificateAlias); config.setCaCertificateAlias(mCaCertificateAlias); } /** * Fill in WifiEnterpriseConfig with information from a SIM credential. * * @param config Instance of {@link WifiEnterpriseConfig} * @param credential Instance of {@link SimCredential} */ private void buildEnterpriseConfigForSimCredential(WifiEnterpriseConfig config, Credential.SimCredential credential) { int eapMethod = WifiEnterpriseConfig.Eap.NONE; switch(credential.getEapType()) { case EAPConstants.EAP_SIM: eapMethod = WifiEnterpriseConfig.Eap.SIM; break; case EAPConstants.EAP_AKA: eapMethod = WifiEnterpriseConfig.Eap.AKA; break; case EAPConstants.EAP_AKA_PRIME: eapMethod = WifiEnterpriseConfig.Eap.AKA_PRIME; break; default: // Should never happen since this is already validated when the provider is // added. Log.wtf(TAG, "Unsupported EAP Method: " + credential.getEapType()); break; } config.setEapMethod(eapMethod); config.setPlmn(credential.getImsi()); } private static void setAnonymousIdentityToNaiRealm(WifiEnterpriseConfig config, String realm) { /** * Set WPA supplicant's anonymous identity field to a string containing the NAI realm, so * that this value will be sent to the EAP server as part of the EAP-Response/ Identity * packet. WPA supplicant will reset this field after using it for the EAP-Response/Identity * packet, and revert to using the (real) identity field for subsequent transactions that * request an identity (e.g. in EAP-TTLS). * * This NAI realm value (the portion of the identity after the '@') is used to tell the * AAA server which AAA/H to forward packets to. The hardcoded username, "anonymous", is a * placeholder that is not used--it is set to this value by convention. See Section 5.1 of * RFC3748 for more details. * * NOTE: we do not set this value for EAP-SIM/AKA/AKA', since the EAP server expects the * EAP-Response/Identity packet to contain an actual, IMSI-based identity, in order to * identify the device. */ config.setAnonymousIdentity("anonymous@" + realm); } /** * Helper function for creating a * {@link android.net.wifi.hotspot2.pps.Credential.UserCredential} from the given * {@link WifiEnterpriseConfig} * * @param config The enterprise configuration containing the credential * @return {@link android.net.wifi.hotspot2.pps.Credential.UserCredential} */ private static Credential.UserCredential buildUserCredentialFromEnterpriseConfig( WifiEnterpriseConfig config) { Credential.UserCredential userCredential = new Credential.UserCredential(); userCredential.setEapType(EAPConstants.EAP_TTLS); if (TextUtils.isEmpty(config.getIdentity())) { Log.e(TAG, "Missing username for user credential"); return null; } userCredential.setUsername(config.getIdentity()); if (TextUtils.isEmpty(config.getPassword())) { Log.e(TAG, "Missing password for user credential"); return null; } String encodedPassword = new String(Base64.encode(config.getPassword().getBytes(StandardCharsets.UTF_8), Base64.DEFAULT), StandardCharsets.UTF_8); userCredential.setPassword(encodedPassword); switch(config.getPhase2Method()) { case WifiEnterpriseConfig.Phase2.PAP: userCredential.setNonEapInnerMethod(Credential.UserCredential.AUTH_METHOD_PAP); break; case WifiEnterpriseConfig.Phase2.MSCHAP: userCredential.setNonEapInnerMethod(Credential.UserCredential.AUTH_METHOD_MSCHAP); break; case WifiEnterpriseConfig.Phase2.MSCHAPV2: userCredential.setNonEapInnerMethod(Credential.UserCredential.AUTH_METHOD_MSCHAPV2); break; default: Log.e(TAG, "Unsupported phase2 method for TTLS: " + config.getPhase2Method()); return null; } return userCredential; } /** * Helper function for creating a * {@link android.net.wifi.hotspot2.pps.Credential.SimCredential} from the given * {@link WifiEnterpriseConfig} * * @param eapType The EAP type of the SIM credential * @param config The enterprise configuration containing the credential * @return {@link android.net.wifi.hotspot2.pps.Credential.SimCredential} */ private static Credential.SimCredential buildSimCredentialFromEnterpriseConfig( int eapType, WifiEnterpriseConfig config) { Credential.SimCredential simCredential = new Credential.SimCredential(); if (TextUtils.isEmpty(config.getPlmn())) { Log.e(TAG, "Missing IMSI for SIM credential"); return null; } simCredential.setImsi(config.getPlmn()); simCredential.setEapType(eapType); return simCredential; } }