diff options
12 files changed, 1319 insertions, 78 deletions
diff --git a/service/java/com/android/server/wifi/IMSIParameter.java b/service/java/com/android/server/wifi/IMSIParameter.java index deea870bc..ab9ec0a28 100644 --- a/service/java/com/android/server/wifi/IMSIParameter.java +++ b/service/java/com/android/server/wifi/IMSIParameter.java @@ -1,8 +1,37 @@ +/* + * 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; -import java.io.IOException; +import android.text.TextUtils; +/** + * Class for storing an IMSI (International Mobile Subscriber Identity) parameter. The IMSI + * contains number (up to 15) of numerical digits. When an IMSI ends with a '*', the specified + * IMSI is a prefix. + */ public class IMSIParameter { + private static final int MAX_IMSI_LENGTH = 15; + + /** + * MCC (Mobile Country Code) is a 3 digit number and MNC (Mobile Network Code) is also a 3 + * digit number. + */ + private static final int MCC_MNC_LENGTH = 6; + private final String mImsi; private final boolean mPrefix; @@ -11,11 +40,22 @@ public class IMSIParameter { mPrefix = prefix; } - public IMSIParameter(String imsi) throws IOException { - if (imsi == null || imsi.length() == 0) { - throw new IOException("Bad IMSI: '" + imsi + "'"); + /** + * Build an IMSIParameter object from the given string. A null will be returned for a + * malformed string. + * + * @param imsi The IMSI string + * @return {@link IMSIParameter} + */ + public static IMSIParameter build(String imsi) { + if (TextUtils.isEmpty(imsi)) { + return null; + } + if (imsi.length() > MAX_IMSI_LENGTH) { + return null; } + // Detect the first non-digit character. int nonDigit; char stopChar = '\0'; for (nonDigit = 0; nonDigit < imsi.length(); nonDigit++) { @@ -26,48 +66,55 @@ public class IMSIParameter { } if (nonDigit == imsi.length()) { - mImsi = imsi; - mPrefix = false; + // Full IMSI. + return new IMSIParameter(imsi, false); } else if (nonDigit == imsi.length()-1 && stopChar == '*') { - mImsi = imsi.substring(0, nonDigit); - mPrefix = true; - } - else { - throw new IOException("Bad IMSI: '" + imsi + "'"); + // IMSI prefix. + return new IMSIParameter(imsi.substring(0, nonDigit), true); } + return null; } - public boolean matches(String fullIMSI) { + /** + * Perform matching against the given full IMSI. + * + * @param fullIMSI The full IMSI to match against + * @return true if matched + */ + public boolean matchesImsi(String fullIMSI) { + if (fullIMSI == null) { + return false; + } + if (mPrefix) { + // Prefix matching. return mImsi.regionMatches(false, 0, fullIMSI, 0, mImsi.length()); - } - else { + } else { + // Exact matching. return mImsi.equals(fullIMSI); } } + /** + * Perform matching against the given MCC-MNC (Mobile Country Code and Mobile Network + * Code) combination. + * + * @param mccMnc The MCC-MNC to match against + * @return true if matched + */ public boolean matchesMccMnc(String mccMnc) { - if (mPrefix) { - // For a prefix match, the entire prefix must match the mcc+mnc - return mImsi.regionMatches(false, 0, mccMnc, 0, mImsi.length()); + if (mccMnc == null) { + return false; } - else { - // For regular match, the entire length of mcc+mnc must match this IMSI - return mImsi.regionMatches(false, 0, mccMnc, 0, mccMnc.length()); + if (mccMnc.length() != MCC_MNC_LENGTH) { + return false; } - } - - public boolean isPrefix() { - return mPrefix; - } - - public String getImsi() { - return mImsi; - } - - public int prefixLength() { - return mImsi.length(); + int checkLength = MCC_MNC_LENGTH; + if (mPrefix && mImsi.length() < MCC_MNC_LENGTH) { + checkLength = mImsi.length(); + } + return mImsi.regionMatches(false, 0, mccMnc, 0, checkLength); } @Override @@ -75,12 +122,12 @@ public class IMSIParameter { if (this == thatObject) { return true; } - else if (thatObject == null || getClass() != thatObject.getClass()) { + if (!(thatObject instanceof IMSIParameter)) { return false; } IMSIParameter that = (IMSIParameter) thatObject; - return mPrefix == that.mPrefix && mImsi.equals(that.mImsi); + return mPrefix == that.mPrefix && TextUtils.equals(mImsi, that.mImsi); } @Override diff --git a/service/java/com/android/server/wifi/SIMAccessor.java b/service/java/com/android/server/wifi/SIMAccessor.java index d4decc4e5..21bfb9c51 100644 --- a/service/java/com/android/server/wifi/SIMAccessor.java +++ b/service/java/com/android/server/wifi/SIMAccessor.java @@ -23,7 +23,7 @@ public class SIMAccessor { List<String> imsis = new ArrayList<>(); for (int subId : mSubscriptionManager.getActiveSubscriptionIdList()) { String imsi = mTelephonyManager.getSubscriberId(subId); - if (imsi != null && mccMnc.matches(imsi)) { + if (imsi != null && mccMnc.matchesImsi(imsi)) { imsis.add(imsi); } } diff --git a/service/java/com/android/server/wifi/hotspot2/ANQPMatcher.java b/service/java/com/android/server/wifi/hotspot2/ANQPMatcher.java new file mode 100644 index 000000000..649534662 --- /dev/null +++ b/service/java/com/android/server/wifi/hotspot2/ANQPMatcher.java @@ -0,0 +1,257 @@ +/* + * 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 com.android.server.wifi.IMSIParameter; +import com.android.server.wifi.hotspot2.anqp.CellularNetwork; +import com.android.server.wifi.hotspot2.anqp.DomainNameElement; +import com.android.server.wifi.hotspot2.anqp.NAIRealmData; +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.EAPMethod; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Utility class for providing matching functions against ANQP elements. + */ +public class ANQPMatcher { + /** + * Match the domain names in the ANQP element against the provider's FQDN and SIM credential. + * The Domain Name ANQP element might contain domains for 3GPP network (e.g. + * wlan.mnc*.mcc*.3gppnetwork.org), so we should match that against the provider's SIM + * credential if one is provided. + * + * @param element The Domain Name ANQP element + * @param fqdn The FQDN to compare against + * @param imsiParam The IMSI parameter of the provider + * @param simImsiList The list of IMSI from the installed SIM cards that matched provider's + * IMSI parameter + * @return true if a match is found + */ + public static boolean matchDomainName(DomainNameElement element, String fqdn, + IMSIParameter imsiParam, List<String> simImsiList) { + if (element == null) { + return false; + } + + for (String domain : element.getDomains()) { + if (DomainMatcher.arg2SubdomainOfArg1(fqdn, domain)) { + return true; + } + + // Try to retrieve the MCC-MNC string from the domain (for 3GPP network domain) and + // match against the provider's SIM credential. + if (matchMccMnc(Utils.getMccMnc(Utils.splitDomain(domain)), imsiParam, simImsiList)) { + return true; + } + } + return false; + } + + /** + * Match the roaming consortium OIs in the ANQP element against the roaming consortium OIs + * of a provider. + * + * @param element The Roaming Consortium ANQP element + * @param providerOIs The roaming consortium OIs of the provider + * @return true if a match is found + */ + public static boolean matchRoamingConsortium(RoamingConsortiumElement element, + long[] providerOIs) { + if (element == null) { + return false; + } + if (providerOIs == null) { + return false; + } + List<Long> rcOIs = element.getOIs(); + for (long oi : providerOIs) { + if (rcOIs.contains(oi)) { + return true; + } + } + return false; + } + + /** + * Match the NAI realm in the ANQP element against the realm and authentication method of + * a provider. + * + * @param element The NAI Realm ANQP element + * @param realm The realm of the provider's credential + * @param eapMethodID The EAP Method ID of the provider's credential + * @param authParam The authentication parameter of the provider's credential + * @return an integer indicating the match status + */ + public static int matchNAIRealm(NAIRealmElement element, String realm, int eapMethodID, + AuthParam authParam) { + if (element == null || element.getRealmDataList().isEmpty()) { + return AuthMatch.INDETERMINATE; + } + + int bestMatch = AuthMatch.NONE; + for (NAIRealmData realmData : element.getRealmDataList()) { + int match = matchNAIRealmData(realmData, realm, eapMethodID, authParam); + if (match > bestMatch) { + bestMatch = match; + if (bestMatch == AuthMatch.EXACT) { + break; + } + } + } + return bestMatch; + } + + /** + * Match the 3GPP Network in the ANQP element against the SIM credential of a provider. + * + * @param element 3GPP Network ANQP element + * @param imsiParam The IMSI parameter of the provider's SIM credential + * @param simImsiList The list of IMSI from the installed SIM cards that matched provider's + * IMSI parameter + * @return true if a matched is found + */ + public static boolean matchThreeGPPNetwork(ThreeGPPNetworkElement element, + IMSIParameter imsiParam, List<String> simImsiList) { + if (element == null) { + return false; + } + for (CellularNetwork network : element.getNetworks()) { + if (matchCellularNetwork(network, imsiParam, simImsiList)) { + return true; + } + } + return false; + } + + /** + * Match the given NAI Realm data against the realm and authentication method of a provider. + * + * @param realmData The NAI Realm data + * @param realm The realm of the provider's credential + * @param eapMethodID The EAP Method ID of the provider's credential + * @param authParam The authentication parameter of the provider's credential + * @return an integer indicating the match status + */ + private static int matchNAIRealmData(NAIRealmData realmData, String realm, int eapMethodID, + AuthParam authParam) { + // Check for realm domain name match. + int realmMatch = AuthMatch.NONE; + for (String realmStr : realmData.getRealms()) { + if (DomainMatcher.arg2SubdomainOfArg1(realm, realmStr)) { + realmMatch = AuthMatch.REALM; + break; + } + } + + if (realmMatch == AuthMatch.NONE || realmData.getEAPMethods().isEmpty()) { + return realmMatch; + } + + // Check for EAP method match. + int eapMethodMatch = AuthMatch.NONE; + for (EAPMethod eapMethod : realmData.getEAPMethods()) { + eapMethodMatch = matchEAPMethod(eapMethod, eapMethodID, authParam); + if (eapMethodMatch != AuthMatch.NONE) { + break; + } + } + + if (eapMethodMatch == AuthMatch.NONE) { + return AuthMatch.NONE; + } + return realmMatch | eapMethodMatch; + } + + /** + * Match the given EAPMethod against the authentication method of a provider. + * + * @param method The EAP Method + * @param eapMethodID The EAP Method ID of the provider's credential + * @param authParam The authentication parameter of the provider's credential + * @return an integer indicating the match status + */ + private static int matchEAPMethod(EAPMethod method, int eapMethodID, AuthParam authParam) { + if (method.getEAPMethodID() != eapMethodID) { + return AuthMatch.NONE; + } + // Check for authentication parameter match. + if (authParam != null) { + Map<Integer, Set<AuthParam>> authParams = method.getAuthParams(); + Set<AuthParam> paramSet = authParams.get(authParam.getAuthTypeID()); + if (paramSet == null || !paramSet.contains(authParam)) { + return AuthMatch.NONE; + } + return AuthMatch.METHOD_PARAM; + } + return AuthMatch.METHOD; + } + + /** + * Match a cellular network information in the 3GPP Network ANQP element against the SIM + * credential of a provider. + * + * @param network The cellular network that contained list of PLMNs + * @param imsiParam IMSI parameter of the provider + * @param simImsiList The list of IMSI from the installed SIM cards that matched provider's + * IMSI parameter + * @return true if a match is found + */ + private static boolean matchCellularNetwork(CellularNetwork network, IMSIParameter imsiParam, + List<String> simImsiList) { + for (String plmn : network.getPlmns()) { + if (matchMccMnc(plmn, imsiParam, simImsiList)) { + return true; + } + } + return false; + } + + /** + * Match a MCC-MNC against the SIM credential of a provider. + * + * @param mccMnc The string containing MCC-MNC + * @param imsiParam The IMSI parameter of the provider + * @param simImsiList The list of IMSI from the installed SIM cards that matched provider's + * IMSI parameter + * @return true if a match is found + */ + private static boolean matchMccMnc(String mccMnc, IMSIParameter imsiParam, + List<String> simImsiList) { + if (imsiParam == null || simImsiList == null) { + return false; + } + // Match against the IMSI parameter in the provider. + if (!imsiParam.matchesMccMnc(mccMnc)) { + return false; + } + // Additional check for verifying the match with IMSIs from the SIM cards, since the IMSI + // parameter might not contain the full 6-digit MCC MNC (e.g. IMSI parameter is an IMSI + // prefix that contained less than 6-digit of numbers "12345*"). + for (String imsi : simImsiList) { + if (imsi.startsWith(mccMnc)) { + return true; + } + } + return false; + } +} diff --git a/service/java/com/android/server/wifi/hotspot2/AuthMatch.java b/service/java/com/android/server/wifi/hotspot2/AuthMatch.java index cd988b548..3abf35fb6 100644 --- a/service/java/com/android/server/wifi/hotspot2/AuthMatch.java +++ b/service/java/com/android/server/wifi/hotspot2/AuthMatch.java @@ -1,3 +1,18 @@ +/* + * 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; /** @@ -9,13 +24,13 @@ package com.android.server.wifi.hotspot2; * must be maintained accordingly. */ public abstract class AuthMatch { - public static final int None = -1; - public static final int Indeterminate = 0; - public static final int Realm = 0x04; - public static final int Method = 0x02; - public static final int Param = 0x01; - public static final int MethodParam = Method | Param; - public static final int Exact = Realm | Method | Param; + public static final int NONE = -1; + public static final int INDETERMINATE = 0; + public static final int REALM = 0x04; + public static final int METHOD = 0x02; + public static final int PARAM = 0x01; + public static final int METHOD_PARAM = METHOD | PARAM; + public static final int EXACT = REALM | METHOD | PARAM; public static String toString(int match) { if (match < 0) { @@ -26,13 +41,13 @@ public abstract class AuthMatch { } StringBuilder sb = new StringBuilder(); - if ((match & Realm) != 0) { + if ((match & REALM) != 0) { sb.append("Realm"); } - if ((match & Method) != 0) { + if ((match & METHOD) != 0) { sb.append("Method"); } - if ((match & Param) != 0) { + if ((match & PARAM) != 0) { sb.append("Param"); } return sb.toString(); diff --git a/service/java/com/android/server/wifi/hotspot2/PasspointManager.java b/service/java/com/android/server/wifi/hotspot2/PasspointManager.java index 31a0b5ef6..88b04a379 100644 --- a/service/java/com/android/server/wifi/hotspot2/PasspointManager.java +++ b/service/java/com/android/server/wifi/hotspot2/PasspointManager.java @@ -43,7 +43,6 @@ import com.android.server.wifi.WifiNative; import com.android.server.wifi.hotspot2.anqp.ANQPElement; import com.android.server.wifi.hotspot2.anqp.Constants; -import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -177,20 +176,16 @@ public class PasspointManager { // Verify IMSI against the IMSI of the installed SIM cards for SIM credential. if (config.credential.simCredential != null) { - try { - if (mSimAccessor.getMatchingImsis( - new IMSIParameter(config.credential.simCredential.imsi)) == null) { - Log.e(TAG, "IMSI does not match any SIM card"); - return false; - } - } catch (IOException e) { + if (mSimAccessor.getMatchingImsis( + IMSIParameter.build(config.credential.simCredential.imsi)) == null) { + Log.e(TAG, "IMSI does not match any SIM card"); return false; } } // Create a provider and install the necessary certificates and keys. PasspointProvider newProvider = mObjectFactory.makePasspointProvider( - config, mKeyStore, mProviderID++); + config, mKeyStore, mSimAccessor, mProviderID++); if (!newProvider.installCertsAndKeys()) { Log.e(TAG, "Failed to install certificates and keys to keystore"); diff --git a/service/java/com/android/server/wifi/hotspot2/PasspointObjectFactory.java b/service/java/com/android/server/wifi/hotspot2/PasspointObjectFactory.java index b5793e7e0..69bf99a9e 100644 --- a/service/java/com/android/server/wifi/hotspot2/PasspointObjectFactory.java +++ b/service/java/com/android/server/wifi/hotspot2/PasspointObjectFactory.java @@ -19,6 +19,7 @@ package com.android.server.wifi.hotspot2; import android.net.wifi.hotspot2.PasspointConfiguration; import com.android.server.wifi.Clock; +import com.android.server.wifi.SIMAccessor; import com.android.server.wifi.WifiKeyStore; import com.android.server.wifi.WifiNative; @@ -48,8 +49,8 @@ public class PasspointObjectFactory{ * @return {@link PasspointProvider} */ public PasspointProvider makePasspointProvider(PasspointConfiguration config, - WifiKeyStore keyStore, long providerId) { - return new PasspointProvider(config, keyStore, providerId); + WifiKeyStore keyStore, SIMAccessor simAccessor, long providerId) { + return new PasspointProvider(config, keyStore, simAccessor, providerId); } /** diff --git a/service/java/com/android/server/wifi/hotspot2/PasspointProvider.java b/service/java/com/android/server/wifi/hotspot2/PasspointProvider.java index a0a52f6ea..61fb0352e 100644 --- a/service/java/com/android/server/wifi/hotspot2/PasspointProvider.java +++ b/service/java/com/android/server/wifi/hotspot2/PasspointProvider.java @@ -16,19 +16,29 @@ package com.android.server.wifi.hotspot2; +import android.net.wifi.EAPConstants; import android.net.wifi.hotspot2.PasspointConfiguration; import android.security.Credentials; 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.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; /** @@ -53,12 +63,37 @@ public class PasspointProvider { private String mClientPrivateKeyAlias; private String mClientCertificateAlias; + private final IMSIParameter mImsiParameter; + private final List<String> mMatchingSIMImsiList; + + private final int mEAPMethodID; + private final AuthParam mAuthParam; + public PasspointProvider(PasspointConfiguration config, WifiKeyStore keyStore, - long providerId) { + SIMAccessor simAccessor, long providerId) { // Maintain a copy of the configuration to avoid it being updated by others. mConfig = new PasspointConfiguration(config); mKeyStore = keyStore; mProviderId = providerId; + + // Setup EAP method and authentication parameter based on the credential. + if (mConfig.credential.userCredential != null) { + mEAPMethodID = EAPConstants.EAP_TTLS; + mAuthParam = new NonEAPInnerAuth(NonEAPInnerAuth.getAuthTypeID( + mConfig.credential.userCredential.nonEapInnerMethod)); + mImsiParameter = null; + mMatchingSIMImsiList = null; + } else if (mConfig.credential.certCredential != null) { + mEAPMethodID = EAPConstants.EAP_TLS; + mAuthParam = null; + mImsiParameter = null; + mMatchingSIMImsiList = null; + } else { + mEAPMethodID = mConfig.credential.simCredential.eapType; + mAuthParam = null; + mImsiParameter = IMSIParameter.build(mConfig.credential.simCredential.imsi); + mMatchingSIMImsiList = simAccessor.getMatchingImsis(mImsiParameter); + } } public PasspointConfiguration getConfig() { @@ -160,12 +195,31 @@ public class PasspointProvider { /** * 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 com.android.server.wifi.hotspot2.PasspointMatch} + * @return {@link PasspointMatch} */ public PasspointMatch match(Map<ANQPElementType, ANQPElement> anqpElements) { - // TODO(b/33246489): To be implemented. - return PasspointMatch.None; + PasspointMatch providerMatch = matchProvider(anqpElements); + + // Perform authentication match against the NAI Realm. + int authMatch = ANQPMatcher.matchNAIRealm( + (NAIRealmElement) anqpElements.get(ANQPElementType.ANQPNAIRealm), + mConfig.credential.realm, 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; } /** @@ -207,4 +261,34 @@ public class PasspointProvider { 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<ANQPElementType, ANQPElement> anqpElements) { + // Domain name matching. + if (ANQPMatcher.matchDomainName( + (DomainNameElement) anqpElements.get(ANQPElementType.ANQPDomName), + mConfig.homeSp.fqdn, mImsiParameter, mMatchingSIMImsiList)) { + return PasspointMatch.HomeProvider; + } + + // Roaming Consortium OI matching. + if (ANQPMatcher.matchRoamingConsortium( + (RoamingConsortiumElement) anqpElements.get(ANQPElementType.ANQPRoamingConsortium), + mConfig.homeSp.roamingConsortiumOIs)) { + return PasspointMatch.RoamingProvider; + } + + // 3GPP Network matching. + if (ANQPMatcher.matchThreeGPPNetwork( + (ThreeGPPNetworkElement) anqpElements.get(ANQPElementType.ANQP3GPPNetwork), + mImsiParameter, mMatchingSIMImsiList)) { + return PasspointMatch.RoamingProvider; + } + return PasspointMatch.None; + } } diff --git a/service/java/com/android/server/wifi/hotspot2/anqp/eap/NonEAPInnerAuth.java b/service/java/com/android/server/wifi/hotspot2/anqp/eap/NonEAPInnerAuth.java index 1afe0d099..b69339315 100644 --- a/service/java/com/android/server/wifi/hotspot2/anqp/eap/NonEAPInnerAuth.java +++ b/service/java/com/android/server/wifi/hotspot2/anqp/eap/NonEAPInnerAuth.java @@ -21,6 +21,8 @@ import com.android.internal.annotations.VisibleForTesting; import java.net.ProtocolException; import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Map; /** * The Non-EAP Inner Authentication Type authentication parameter, IEEE802.11-2012, table 8-188. @@ -30,11 +32,20 @@ import java.nio.ByteBuffer; * 1 */ public class NonEAPInnerAuth extends AuthParam { + public static final int AUTH_TYPE_UNKNOWN = 0; public static final int AUTH_TYPE_PAP = 1; public static final int AUTH_TYPE_CHAP = 2; public static final int AUTH_TYPE_MSCHAP = 3; public static final int AUTH_TYPE_MSCHAPV2 = 4; + private static final Map<String, Integer> AUTH_TYPE_MAP = new HashMap<>(); + static { + AUTH_TYPE_MAP.put("PAP", AUTH_TYPE_PAP); + AUTH_TYPE_MAP.put("CHAP", AUTH_TYPE_CHAP); + AUTH_TYPE_MAP.put("MS-CHAP", AUTH_TYPE_MSCHAP); + AUTH_TYPE_MAP.put("MS-CHAP-V2", AUTH_TYPE_MSCHAPV2); + } + @VisibleForTesting public static final int EXPECTED_LENGTH_VALUE = 1; @@ -61,6 +72,19 @@ public class NonEAPInnerAuth extends AuthParam { return new NonEAPInnerAuth(authType); } + /** + * Convert an authentication type string to an integer representation. + * + * @param typeStr The string of authentication type + * @return int + */ + public static int getAuthTypeID(String typeStr) { + if (AUTH_TYPE_MAP.containsKey(typeStr)) { + return AUTH_TYPE_MAP.get(typeStr).intValue(); + } + return AUTH_TYPE_UNKNOWN; + } + @Override public boolean equals(Object thatObject) { if (thatObject == this) { diff --git a/tests/wifitests/src/com/android/server/wifi/IMSIParameterTest.java b/tests/wifitests/src/com/android/server/wifi/IMSIParameterTest.java new file mode 100644 index 000000000..d99d8a51b --- /dev/null +++ b/tests/wifitests/src/com/android/server/wifi/IMSIParameterTest.java @@ -0,0 +1,161 @@ +/* + * 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; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.test.suitebuilder.annotation.SmallTest; + +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +/** + * Unit tests for {@link com.android.server.wifi.IMSIParameter}. + */ +@SmallTest +public class IMSIParameterTest { + /** + * Data points for testing function {@link IMSIParameter#build}. + */ + private static final Map<String, IMSIParameter> BUILD_PARAM_TEST_MAP = new HashMap<>(); + static { + BUILD_PARAM_TEST_MAP.put(null, null); + BUILD_PARAM_TEST_MAP.put("", null); + BUILD_PARAM_TEST_MAP.put("1234a123", null); // IMSI contained invalid character. + BUILD_PARAM_TEST_MAP.put("1234567890123451", null); // IMSI exceeding max length. + BUILD_PARAM_TEST_MAP.put("123456789012345", new IMSIParameter("123456789012345", false)); + BUILD_PARAM_TEST_MAP.put("1234*", new IMSIParameter("1234", true)); + } + + /** + * Verify the expectations of {@link IMSIParameter#build} function using the predefined + * test data {@link #BUILD_PARAM_TEST_MAP}. + * + * @throws Exception + */ + @Test + public void verifyBuildIMSIParameter() throws Exception { + for (Map.Entry<String, IMSIParameter> entry : BUILD_PARAM_TEST_MAP.entrySet()) { + assertEquals(entry.getValue(), IMSIParameter.build(entry.getKey())); + } + } + + /** + * Verify that attempt to match a null IMSI will not cause any crash and should return false. + * + * @throws Exception + */ + @Test + public void matchesNullImsi() throws Exception { + IMSIParameter param = new IMSIParameter("1234", false); + assertFalse(param.matchesImsi(null)); + } + + /** + * Verify that an IMSIParameter containing a full IMSI will only match against an IMSI of the + * same value. + * + * @throws Exception + */ + @Test + public void matchesImsiWithFullImsi() throws Exception { + IMSIParameter param = new IMSIParameter("1234", false); + + // Full IMSI requires exact matching. + assertFalse(param.matchesImsi("123")); + assertFalse(param.matchesImsi("12345")); + assertTrue(param.matchesImsi("1234")); + } + + /** + * Verify that an IMSIParameter containing a prefix IMSI will match against any IMSI that + * starts with the same prefix. + * + * @throws Exception + */ + @Test + public void matchesImsiWithPrefixImsi() throws Exception { + IMSIParameter param = new IMSIParameter("1234", true); + + // Prefix IMSI will match any IMSI that starts with the same prefix. + assertFalse(param.matchesImsi("123")); + assertTrue(param.matchesImsi("12345")); + assertTrue(param.matchesImsi("1234")); + } + + /** + * Verify that attempt to match a null MCC-MNC will not cause any crash and should return + * false. + * + * @throws Exception + */ + @Test + public void matchesNullMccMnc() throws Exception { + IMSIParameter param = new IMSIParameter("1234", false); + assertFalse(param.matchesMccMnc(null)); + } + + /** + * Verify that an IMSIParameter containing a full IMSI will only match against a 6 digit + * MCC-MNC that is a prefix of the IMSI. + * + * @throws Exception + */ + @Test + public void matchesMccMncFullImsi() throws Exception { + IMSIParameter param = new IMSIParameter("1234567890", false); + + assertFalse(param.matchesMccMnc("1234567")); // Invalid length for MCC-MNC + assertFalse(param.matchesMccMnc("12345")); // Invalid length for MCC-MNC + assertTrue(param.matchesMccMnc("123456")); + } + + /** + * Verify that an IMSIParameter containing an IMSI prefix that's less than 6 digits + * will match against any 6-digit MCC-MNC that starts with the same prefix. + * + * @throws Exception + */ + @Test + public void matchesMccMncWithPrefixImsiLessThanMccMncLength() throws Exception { + IMSIParameter param = new IMSIParameter("12345", true); + + assertFalse(param.matchesMccMnc("123448")); // Prefix mismatch + assertFalse(param.matchesMccMnc("12345")); // Invalid length for MCC-MNC + assertFalse(param.matchesMccMnc("1234567")); // Invalid length for MCC-MNC + assertTrue(param.matchesMccMnc("123457")); + assertTrue(param.matchesMccMnc("123456")); + } + + /** + * Verify that an IMSIParameter containing an IMSI prefix that's more than 6 digits + * will only match against a 6-digit MCC-MNC that matches the first 6-digit of the prefix. + * + * @throws Exception + */ + @Test + public void matchesMccMncWithPrefixImsiMoreThanMccMncLength() throws Exception { + IMSIParameter param = new IMSIParameter("1234567890", true); + assertFalse(param.matchesMccMnc("12345")); // Invalid length for MCC-MNC + assertFalse(param.matchesMccMnc("1234567")); // Invalid length for MCC-MNC + assertTrue(param.matchesMccMnc("123456")); + } +} diff --git a/tests/wifitests/src/com/android/server/wifi/hotspot2/ANQPMatcherTest.java b/tests/wifitests/src/com/android/server/wifi/hotspot2/ANQPMatcherTest.java new file mode 100644 index 000000000..5fbd4aaf7 --- /dev/null +++ b/tests/wifitests/src/com/android/server/wifi/hotspot2/ANQPMatcherTest.java @@ -0,0 +1,338 @@ +/* + * 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.net.wifi.EAPConstants; +import android.test.suitebuilder.annotation.SmallTest; + +import com.android.server.wifi.IMSIParameter; +import com.android.server.wifi.hotspot2.anqp.CellularNetwork; +import com.android.server.wifi.hotspot2.anqp.DomainNameElement; +import com.android.server.wifi.hotspot2.anqp.NAIRealmData; +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.EAPMethod; +import com.android.server.wifi.hotspot2.anqp.eap.InnerAuthEAP; +import com.android.server.wifi.hotspot2.anqp.eap.NonEAPInnerAuth; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Unit tests for {@link com.android.server.wifi.hotspot2.ANQPMatcher}. + */ +@SmallTest +public class ANQPMatcherTest { + /** + * Verify that domain name match will fail when a null Domain Name ANQP element is provided. + * + * @throws Exception + */ + @Test + public void matchDomainNameWithNullElement() throws Exception { + assertFalse(ANQPMatcher.matchDomainName(null, "test.com", null, null)); + } + + /** + * Verify that domain name match will succeed when the specified FQDN matches a domain name + * in the Domain Name ANQP element. + * + * @throws Exception + */ + @Test + public void matchDomainNameUsingFQDN() throws Exception { + String fqdn = "test.com"; + String[] domains = new String[] {fqdn}; + DomainNameElement element = new DomainNameElement(Arrays.asList(domains)); + assertTrue(ANQPMatcher.matchDomainName(element, fqdn, null, null)); + } + + /** + * Verify that domain name match will succeed when the specified IMSI parameter and IMSI list + * matches a 3GPP network domain in the Domain Name ANQP element. + * + * @throws Exception + */ + @Test + public void matchDomainNameUsingIMSI() throws Exception { + IMSIParameter imsiParam = new IMSIParameter("1234", true); + List<String> simImsiList = Arrays.asList(new String[] {"123457890", "123498723"}); + // 3GPP network domain with MCC=123 and MNC=456. + String[] domains = new String[] {"wlan.mnc457.mcc123.3gppnetwork.org"}; + DomainNameElement element = new DomainNameElement(Arrays.asList(domains)); + assertTrue(ANQPMatcher.matchDomainName(element, null, imsiParam, simImsiList)); + } + + /** + * Verify that roaming consortium match will fail when a null Roaming Consortium ANQP + * element is provided. + * + * @throws Exception + */ + @Test + public void matchRoamingConsortiumWithNullElement() throws Exception { + assertFalse(ANQPMatcher.matchRoamingConsortium(null, new long[0])); + } + + /** + * Verify that a roaming consortium match will succeed when the specified OI matches + * an OI in the Roaming Consortium ANQP element. + * + * @throws Exception + */ + @Test + public void matchRoamingConsortium() throws Exception { + long oi = 0x1234L; + RoamingConsortiumElement element = + new RoamingConsortiumElement(Arrays.asList(new Long[] {oi})); + assertTrue(ANQPMatcher.matchRoamingConsortium(element, new long[] {oi})); + } + + /** + * Verify that an indeterminate match will be returned when matching a null NAI Realm + * ANQP element. + * + * @throws Exception + */ + @Test + public void matchNAIRealmWithNullElement() throws Exception { + assertEquals(AuthMatch.INDETERMINATE, ANQPMatcher.matchNAIRealm(null, "test.com", + EAPConstants.EAP_TLS, new InnerAuthEAP(EAPConstants.EAP_TTLS))); + } + + /** + * Verify that an indeterminate match will be returned when matching a NAI Realm + * ANQP element contained no NAI realm data. + * + * @throws Exception + */ + @Test + public void matchNAIRealmWithEmtpyRealmData() throws Exception { + NAIRealmElement element = new NAIRealmElement(new ArrayList<NAIRealmData>()); + assertEquals(AuthMatch.INDETERMINATE, ANQPMatcher.matchNAIRealm(element, "test.com", + EAPConstants.EAP_TLS, null)); + } + + /** + * Verify that a realm match will be returned when the specified realm matches a realm + * in the NAI Realm ANQP element with no EAP methods. + * + * @throws Exception + */ + @Test + public void matchNAIRealmWithRealmMatch() throws Exception { + String realm = "test.com"; + NAIRealmData realmData = new NAIRealmData( + Arrays.asList(new String[] {realm}), new ArrayList<EAPMethod>()); + NAIRealmElement element = new NAIRealmElement( + Arrays.asList(new NAIRealmData[] {realmData})); + assertEquals(AuthMatch.REALM, ANQPMatcher.matchNAIRealm(element, realm, + EAPConstants.EAP_TLS, null)); + } + + /** + * Verify that a realm and method match will be returned when the specified realm and EAP + * method matches a realm in the NAI Realm ANQP element. + * + * @throws Exception + */ + @Test + public void matchNAIRealmWithRealmMethodMatch() throws Exception { + // Test data. + String realm = "test.com"; + int eapMethodID = EAPConstants.EAP_TLS; + + // Setup NAI Realm element. + EAPMethod method = new EAPMethod(eapMethodID, new HashMap<Integer, Set<AuthParam>>()); + NAIRealmData realmData = new NAIRealmData( + Arrays.asList(new String[] {realm}), Arrays.asList(new EAPMethod[] {method})); + NAIRealmElement element = new NAIRealmElement( + Arrays.asList(new NAIRealmData[] {realmData})); + + assertEquals(AuthMatch.REALM | AuthMatch.METHOD, + ANQPMatcher.matchNAIRealm(element, realm, eapMethodID, null)); + } + + /** + * Verify that an exact match will be returned when the specified realm, EAP + * method, and the authentication parameter matches a realm with the associated EAP method and + * authentication parameter in the NAI Realm ANQP element. + * + * @throws Exception + */ + @Test + public void matchNAIRealmWithExactMatch() throws Exception { + // Test data. + String realm = "test.com"; + int eapMethodID = EAPConstants.EAP_TTLS; + NonEAPInnerAuth authParam = new NonEAPInnerAuth(NonEAPInnerAuth.AUTH_TYPE_MSCHAP); + Set<AuthParam> authSet = new HashSet<>(); + authSet.add(authParam); + Map<Integer, Set<AuthParam>> authMap = new HashMap<>(); + authMap.put(authParam.getAuthTypeID(), authSet); + + // Setup NAI Realm element. + EAPMethod method = new EAPMethod(eapMethodID, authMap); + NAIRealmData realmData = new NAIRealmData( + Arrays.asList(new String[] {realm}), Arrays.asList(new EAPMethod[] {method})); + NAIRealmElement element = new NAIRealmElement( + Arrays.asList(new NAIRealmData[] {realmData})); + + assertEquals(AuthMatch.EXACT, + ANQPMatcher.matchNAIRealm(element, realm, eapMethodID, authParam)); + } + + /** + * Verify that a mismatch (AuthMatch.NONE) will be returned when the specified EAP method + * doesn't match with the corresponding EAP method in the NAI Realm ANQP element. + * + * @throws Exception + */ + @Test + public void matchNAIRealmWithEAPMethodMismatch() throws Exception { + // Test data. + String realm = "test.com"; + int eapMethodID = EAPConstants.EAP_TTLS; + NonEAPInnerAuth authParam = new NonEAPInnerAuth(NonEAPInnerAuth.AUTH_TYPE_MSCHAP); + Set<AuthParam> authSet = new HashSet<>(); + authSet.add(authParam); + Map<Integer, Set<AuthParam>> authMap = new HashMap<>(); + authMap.put(authParam.getAuthTypeID(), authSet); + + // Setup NAI Realm element. + EAPMethod method = new EAPMethod(eapMethodID, authMap); + NAIRealmData realmData = new NAIRealmData( + Arrays.asList(new String[] {realm}), Arrays.asList(new EAPMethod[] {method})); + NAIRealmElement element = new NAIRealmElement( + Arrays.asList(new NAIRealmData[] {realmData})); + + assertEquals(AuthMatch.NONE, + ANQPMatcher.matchNAIRealm(element, realm, EAPConstants.EAP_TLS, null)); + } + + /** + * Verify that a mismatch (AuthMatch.NONE) will be returned when the specified authentication + * parameter doesn't match with the corresponding authentication parameter in the NAI Realm + * ANQP element. + * + * @throws Exception + */ + @Test + public void matchNAIRealmWithAuthTypeMismatch() throws Exception { + // Test data. + String realm = "test.com"; + int eapMethodID = EAPConstants.EAP_TTLS; + NonEAPInnerAuth authParam = new NonEAPInnerAuth(NonEAPInnerAuth.AUTH_TYPE_MSCHAP); + Set<AuthParam> authSet = new HashSet<>(); + authSet.add(authParam); + Map<Integer, Set<AuthParam>> authMap = new HashMap<>(); + authMap.put(authParam.getAuthTypeID(), authSet); + + // Setup NAI Realm element. + EAPMethod method = new EAPMethod(eapMethodID, authMap); + NAIRealmData realmData = new NAIRealmData( + Arrays.asList(new String[] {realm}), Arrays.asList(new EAPMethod[] {method})); + NAIRealmElement element = new NAIRealmElement( + Arrays.asList(new NAIRealmData[] {realmData})); + + // Mismatch in authentication type. + assertEquals(AuthMatch.NONE, + ANQPMatcher.matchNAIRealm(element, realm, EAPConstants.EAP_TTLS, + new NonEAPInnerAuth(NonEAPInnerAuth.AUTH_TYPE_PAP))); + } + + /** + * Verify that 3GPP Network match will fail when a null element is provided. + * + * @throws Exception + */ + @Test + public void matchThreeGPPNetworkWithNullElement() throws Exception { + IMSIParameter imsiParam = new IMSIParameter("1234", true); + List<String> simImsiList = Arrays.asList(new String[] {"123456789", "123498723"}); + assertFalse(ANQPMatcher.matchThreeGPPNetwork(null, imsiParam, simImsiList)); + } + + /** + * Verify that 3GPP network will succeed when the given 3GPP Network ANQP element contained + * a MCC-MNC that matches the both IMSI parameter and an IMSI from the IMSI list. + * + * @throws Exception + */ + @Test + public void matchThreeGPPNetwork() throws Exception { + IMSIParameter imsiParam = new IMSIParameter("1234", true); + List<String> simImsiList = Arrays.asList(new String[] {"123456789", "123498723"}); + + CellularNetwork network = new CellularNetwork(Arrays.asList(new String[] {"123456"})); + ThreeGPPNetworkElement element = + new ThreeGPPNetworkElement(Arrays.asList(new CellularNetwork[] {network})); + // The MCC-MNC provided in 3GPP Network ANQP element matches both IMSI parameter + // and an IMSI from the installed SIM card. + assertTrue(ANQPMatcher.matchThreeGPPNetwork(element, imsiParam, simImsiList)); + } + + /** + * Verify that 3GPP network will failed when the given 3GPP Network ANQP element contained + * a MCC-MNC that match the IMSI parameter but not the IMSI list. + * + * @throws Exception + */ + @Test + public void matchThreeGPPNetworkWithoutSimImsiMatch() throws Exception { + IMSIParameter imsiParam = new IMSIParameter("1234", true); + List<String> simImsiList = Arrays.asList(new String[] {"123457890", "123498723"}); + + CellularNetwork network = new CellularNetwork(Arrays.asList(new String[] {"123456"})); + ThreeGPPNetworkElement element = + new ThreeGPPNetworkElement(Arrays.asList(new CellularNetwork[] {network})); + // The MCC-MNC provided in 3GPP Network ANQP element doesn't match any of the IMSIs + // from the installed SIM card. + assertFalse(ANQPMatcher.matchThreeGPPNetwork(element, imsiParam, simImsiList)); + } + + /** + * Verify that 3GPP network will failed when the given 3GPP Network ANQP element contained + * a MCC-MNC that doesn't match with the IMSI parameter. + * + * @throws Exception + */ + @Test + public void matchThreeGPPNetworkWithImsiParamMismatch() throws Exception { + IMSIParameter imsiParam = new IMSIParameter("1234", true); + List<String> simImsiList = Arrays.asList(new String[] {"123457890", "123498723"}); + + CellularNetwork network = new CellularNetwork(Arrays.asList(new String[] {"123356"})); + ThreeGPPNetworkElement element = + new ThreeGPPNetworkElement(Arrays.asList(new CellularNetwork[] {network})); + // The MCC-MNC provided in 3GPP Network ANQP element doesn't match the IMSI parameter. + assertFalse(ANQPMatcher.matchThreeGPPNetwork(element, imsiParam, simImsiList)); + } +} diff --git a/tests/wifitests/src/com/android/server/wifi/hotspot2/PasspointManagerTest.java b/tests/wifitests/src/com/android/server/wifi/hotspot2/PasspointManagerTest.java index af66dc0b7..59dfa3b58 100644 --- a/tests/wifitests/src/com/android/server/wifi/hotspot2/PasspointManagerTest.java +++ b/tests/wifitests/src/com/android/server/wifi/hotspot2/PasspointManagerTest.java @@ -70,6 +70,7 @@ public class PasspointManagerTest { private static final String TEST_FRIENDLY_NAME = "friendly name"; private static final String TEST_REALM = "realm.test.com"; private static final String TEST_IMSI = "1234*"; + private static final IMSIParameter TEST_IMSI_PARAM = IMSIParameter.build(TEST_IMSI); private static final String TEST_SSID = "TestSSID"; private static final long TEST_BSSID = 0x1234L; @@ -166,8 +167,8 @@ public class PasspointManagerTest { config.credential.userCredential.eapType = EAPConstants.EAP_TTLS; config.credential.userCredential.nonEapInnerMethod = "MS-CHAP"; PasspointProvider provider = createMockProvider(config); - when(mObjectFactory.makePasspointProvider(eq(config), eq(mWifiKeyStore), anyLong())) - .thenReturn(provider); + when(mObjectFactory.makePasspointProvider(eq(config), eq(mWifiKeyStore), + eq(mSimAccessor), anyLong())).thenReturn(provider); assertTrue(mManager.addOrUpdateProvider(config)); return provider; @@ -277,8 +278,8 @@ public class PasspointManagerTest { config.credential.userCredential.eapType = EAPConstants.EAP_TTLS; config.credential.userCredential.nonEapInnerMethod = "MS-CHAP"; PasspointProvider provider = createMockProvider(config); - when(mObjectFactory.makePasspointProvider(eq(config), eq(mWifiKeyStore), anyLong())) - .thenReturn(provider); + when(mObjectFactory.makePasspointProvider(eq(config), eq(mWifiKeyStore), + eq(mSimAccessor), anyLong())).thenReturn(provider); assertTrue(mManager.addOrUpdateProvider(config)); verifyInstalledConfig(config); @@ -304,11 +305,10 @@ public class PasspointManagerTest { config.credential.simCredential = new Credential.SimCredential(); config.credential.simCredential.imsi = TEST_IMSI; config.credential.simCredential.eapType = EAPConstants.EAP_SIM; - when(mSimAccessor.getMatchingImsis(new IMSIParameter(TEST_IMSI))) - .thenReturn(new ArrayList<String>()); + when(mSimAccessor.getMatchingImsis(TEST_IMSI_PARAM)).thenReturn(new ArrayList<String>()); PasspointProvider provider = createMockProvider(config); - when(mObjectFactory.makePasspointProvider(eq(config), eq(mWifiKeyStore), anyLong())) - .thenReturn(provider); + when(mObjectFactory.makePasspointProvider(eq(config), eq(mWifiKeyStore), + eq(mSimAccessor), anyLong())).thenReturn(provider); assertTrue(mManager.addOrUpdateProvider(config)); verifyInstalledConfig(config); @@ -335,7 +335,7 @@ public class PasspointManagerTest { config.credential.simCredential = new Credential.SimCredential(); config.credential.simCredential.imsi = TEST_IMSI; config.credential.simCredential.eapType = EAPConstants.EAP_SIM; - when(mSimAccessor.getMatchingImsis(new IMSIParameter(TEST_IMSI))).thenReturn(null); + when(mSimAccessor.getMatchingImsis(TEST_IMSI_PARAM)).thenReturn(null); assertFalse(mManager.addOrUpdateProvider(config)); } @@ -358,11 +358,10 @@ public class PasspointManagerTest { origConfig.credential.simCredential = new Credential.SimCredential(); origConfig.credential.simCredential.imsi = TEST_IMSI; origConfig.credential.simCredential.eapType = EAPConstants.EAP_SIM; - when(mSimAccessor.getMatchingImsis(new IMSIParameter(TEST_IMSI))) - .thenReturn(new ArrayList<String>()); + when(mSimAccessor.getMatchingImsis(TEST_IMSI_PARAM)).thenReturn(new ArrayList<String>()); PasspointProvider origProvider = createMockProvider(origConfig); - when(mObjectFactory.makePasspointProvider(eq(origConfig), eq(mWifiKeyStore), anyLong())) - .thenReturn(origProvider); + when(mObjectFactory.makePasspointProvider(eq(origConfig), eq(mWifiKeyStore), + eq(mSimAccessor), anyLong())).thenReturn(origProvider); assertTrue(mManager.addOrUpdateProvider(origConfig)); verifyInstalledConfig(origConfig); @@ -381,8 +380,8 @@ public class PasspointManagerTest { newConfig.credential.userCredential.eapType = EAPConstants.EAP_TTLS; newConfig.credential.userCredential.nonEapInnerMethod = "MS-CHAP"; PasspointProvider newProvider = createMockProvider(newConfig); - when(mObjectFactory.makePasspointProvider(eq(newConfig), eq(mWifiKeyStore), anyLong())) - .thenReturn(newProvider); + when(mObjectFactory.makePasspointProvider(eq(newConfig), eq(mWifiKeyStore), + eq(mSimAccessor), anyLong())).thenReturn(newProvider); assertTrue(mManager.addOrUpdateProvider(newConfig)); verifyInstalledConfig(newConfig); } @@ -409,8 +408,8 @@ public class PasspointManagerTest { config.credential.userCredential.nonEapInnerMethod = "MS-CHAP"; PasspointProvider provider = mock(PasspointProvider.class); when(provider.installCertsAndKeys()).thenReturn(false); - when(mObjectFactory.makePasspointProvider(eq(config), eq(mWifiKeyStore), anyLong())) - .thenReturn(provider); + when(mObjectFactory.makePasspointProvider(eq(config), eq(mWifiKeyStore), + eq(mSimAccessor), anyLong())).thenReturn(provider); assertFalse(mManager.addOrUpdateProvider(config)); } diff --git a/tests/wifitests/src/com/android/server/wifi/hotspot2/PasspointProviderTest.java b/tests/wifitests/src/com/android/server/wifi/hotspot2/PasspointProviderTest.java index db8a43a9d..0cef02f7b 100644 --- a/tests/wifitests/src/com/android/server/wifi/hotspot2/PasspointProviderTest.java +++ b/tests/wifitests/src/com/android/server/wifi/hotspot2/PasspointProviderTest.java @@ -16,19 +16,34 @@ package com.android.server.wifi.hotspot2; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; +import android.net.wifi.EAPConstants; import android.net.wifi.hotspot2.PasspointConfiguration; import android.net.wifi.hotspot2.pps.Credential; import android.net.wifi.hotspot2.pps.HomeSP; import android.test.suitebuilder.annotation.SmallTest; import com.android.server.wifi.FakeKeys; +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.CellularNetwork; +import com.android.server.wifi.hotspot2.anqp.Constants.ANQPElementType; +import com.android.server.wifi.hotspot2.anqp.DomainNameElement; +import com.android.server.wifi.hotspot2.anqp.NAIRealmData; +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.EAPMethod; +import com.android.server.wifi.hotspot2.anqp.eap.NonEAPInnerAuth; import org.junit.Before; import org.junit.Test; @@ -36,6 +51,11 @@ import org.mockito.Mock; import java.security.MessageDigest; import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; /** * Unit tests for {@link com.android.server.wifi.hotspot2.PasspointProvider}. @@ -48,6 +68,7 @@ public class PasspointProviderTest { private static final String CLIENT_PRIVATE_KEY_ALIAS = "USRPKEY_HS2_12"; @Mock WifiKeyStore mKeyStore; + @Mock SIMAccessor mSimAccessor; PasspointProvider mProvider; /** Sets up test. */ @@ -63,7 +84,7 @@ public class PasspointProviderTest { * @return {@link com.android.server.wifi.hotspot2.PasspointProvider} */ private PasspointProvider createProvider(PasspointConfiguration config) { - return new PasspointProvider(config, mKeyStore, PROVIDER_ID); + return new PasspointProvider(config, mKeyStore, mSimAccessor, PROVIDER_ID); } /** @@ -83,6 +104,59 @@ public class PasspointProviderTest { } /** + * Helper function for creating a Domain Name ANQP element. + * + * @param domains List of domain names + * @return {@link DomainNameElement} + */ + private DomainNameElement createDomainNameElement(String[] domains) { + return new DomainNameElement(Arrays.asList(domains)); + } + + /** + * Helper function for creating a NAI Realm ANQP element. + * + * @param realm The realm of the network + * @param eapMethodID EAP Method ID + * @param authParam Authentication parameter + * @return {@link NAIRealmElement} + */ + private NAIRealmElement createNAIRealmElement(String realm, int eapMethodID, + AuthParam authParam) { + Map<Integer, Set<AuthParam>> authParamMap = new HashMap<>(); + if (authParam != null) { + Set<AuthParam> authSet = new HashSet<>(); + authSet.add(authParam); + authParamMap.put(authParam.getAuthTypeID(), authSet); + } + EAPMethod eapMethod = new EAPMethod(eapMethodID, authParamMap); + NAIRealmData realmData = new NAIRealmData(Arrays.asList(new String[] {realm}), + Arrays.asList(new EAPMethod[] {eapMethod})); + return new NAIRealmElement(Arrays.asList(new NAIRealmData[] {realmData})); + } + + /** + * Helper function for creating a Roaming Consortium ANQP element. + * + * @param rcOIs Roaming consortium OIs + * @return {@link RoamingConsortiumElement} + */ + private RoamingConsortiumElement createRoamingConsortiumElement(Long[] rcOIs) { + return new RoamingConsortiumElement(Arrays.asList(rcOIs)); + } + + /** + * Helper function for creating a 3GPP Network ANQP element. + * + * @param imsiList List of IMSI to be included in a 3GPP Network + * @return {@link ThreeGPPNetworkElement} + */ + private ThreeGPPNetworkElement createThreeGPPNetworkElement(String[] imsiList) { + CellularNetwork network = new CellularNetwork(Arrays.asList(imsiList)); + return new ThreeGPPNetworkElement(Arrays.asList(new CellularNetwork[] {network})); + } + + /** * Verify that modification to the configuration used for creating PasspointProvider * will not change the configuration stored inside the PasspointProvider. * @@ -94,6 +168,8 @@ public class PasspointProviderTest { PasspointConfiguration config = new PasspointConfiguration(); config.homeSp = new HomeSP(); config.homeSp.fqdn = "test1"; + config.credential = new Credential(); + config.credential.userCredential = new Credential.UserCredential(); mProvider = createProvider(config); verifyInstalledConfig(config, true); @@ -115,6 +191,8 @@ public class PasspointProviderTest { PasspointConfiguration config = new PasspointConfiguration(); config.homeSp = new HomeSP(); config.homeSp.fqdn = "test1"; + config.credential = new Credential(); + config.credential.userCredential = new Credential.UserCredential(); mProvider = createProvider(config); verifyInstalledConfig(config, true); @@ -238,4 +316,246 @@ public class PasspointProviderTest { assertTrue(mProvider.getClientPrivateKeyAlias() == null); assertTrue(mProvider.getClientCertificateAlias() == null); } + + /** + * Verify that a provider is a home provider when its FQDN matches a domain name in the + * Domain Name ANQP element and no NAI realm is provided. + * + * @throws Exception + */ + @Test + public void matchFQDNWithoutNAIRealm() throws Exception { + String testDomain = "test.com"; + + // Setup test provider. + PasspointConfiguration config = new PasspointConfiguration(); + config.homeSp = new HomeSP(); + config.homeSp.fqdn = testDomain; + config.credential = new Credential(); + config.credential.userCredential = new Credential.UserCredential(); + config.credential.userCredential.nonEapInnerMethod = "MS-CHAP-V2"; + mProvider = createProvider(config); + + // Setup ANQP elements. + Map<ANQPElementType, ANQPElement> anqpElementMap = new HashMap<>(); + anqpElementMap.put(ANQPElementType.ANQPDomName, + createDomainNameElement(new String[] {testDomain})); + + assertEquals(PasspointMatch.HomeProvider, mProvider.match(anqpElementMap)); + } + + /** + * Verify that a provider is a home provider when its FQDN matches a domain name in the + * Domain Name ANQP element and the provider's credential matches the NAI realm provided. + * + * @throws Exception + */ + @Test + public void matchFQDNWithNAIRealmMatch() throws Exception { + String testDomain = "test.com"; + String testRealm = "realm.com"; + + // Setup test provider. + PasspointConfiguration config = new PasspointConfiguration(); + config.homeSp = new HomeSP(); + config.homeSp.fqdn = testDomain; + config.credential = new Credential(); + config.credential.realm = testRealm; + config.credential.userCredential = new Credential.UserCredential(); + config.credential.userCredential.nonEapInnerMethod = "MS-CHAP-V2"; + mProvider = createProvider(config); + + // Setup Domain Name ANQP element. + Map<ANQPElementType, ANQPElement> anqpElementMap = new HashMap<>(); + anqpElementMap.put(ANQPElementType.ANQPDomName, + createDomainNameElement(new String[] {testDomain})); + anqpElementMap.put(ANQPElementType.ANQPNAIRealm, + createNAIRealmElement(testRealm, EAPConstants.EAP_TTLS, + new NonEAPInnerAuth(NonEAPInnerAuth.AUTH_TYPE_MSCHAPV2))); + + assertEquals(PasspointMatch.HomeProvider, mProvider.match(anqpElementMap)); + } + + /** + * Verify that there is no match when the provider's FQDN matches a domain name in the + * Domain Name ANQP element but the provider's credential doesn't match the authentication + * method provided in the NAI realm. + * + * @throws Exception + */ + @Test + public void matchFQDNWithNAIRealmMismatch() throws Exception { + String testDomain = "test.com"; + String testRealm = "realm.com"; + + // Setup test provider. + PasspointConfiguration config = new PasspointConfiguration(); + config.homeSp = new HomeSP(); + config.homeSp.fqdn = testDomain; + config.credential = new Credential(); + config.credential.realm = testRealm; + config.credential.userCredential = new Credential.UserCredential(); + config.credential.userCredential.nonEapInnerMethod = "MS-CHAP-V2"; + mProvider = createProvider(config); + + // Setup Domain Name ANQP element. + Map<ANQPElementType, ANQPElement> anqpElementMap = new HashMap<>(); + anqpElementMap.put(ANQPElementType.ANQPDomName, + createDomainNameElement(new String[] {testDomain})); + anqpElementMap.put(ANQPElementType.ANQPNAIRealm, + createNAIRealmElement(testRealm, EAPConstants.EAP_TLS, null)); + + assertEquals(PasspointMatch.None, mProvider.match(anqpElementMap)); + } + + /** + * Verify that a provider is a home provider when its SIM credential matches an 3GPP network + * domain name in the Domain Name ANQP element. + * + * @throws Exception + */ + @Test + public void match3GPPNetworkDomainName() throws Exception { + String testImsi = "1234567890"; + + // Setup test provider. + PasspointConfiguration config = new PasspointConfiguration(); + config.homeSp = new HomeSP(); + config.credential = new Credential(); + config.credential.simCredential = new Credential.SimCredential(); + config.credential.simCredential.imsi = testImsi; + when(mSimAccessor.getMatchingImsis(new IMSIParameter(testImsi, false))) + .thenReturn(Arrays.asList(new String[] {testImsi})); + mProvider = createProvider(config); + + // Setup Domain Name ANQP element. + Map<ANQPElementType, ANQPElement> anqpElementMap = new HashMap<>(); + anqpElementMap.put(ANQPElementType.ANQPDomName, + createDomainNameElement(new String[] {"wlan.mnc456.mcc123.3gppnetwork.org"})); + + assertEquals(PasspointMatch.HomeProvider, mProvider.match(anqpElementMap)); + } + + /** + * Verify that a provider is a roaming provider when a roaming consortium OI matches an OI + * in the roaming consortium ANQP element. + * + * @throws Exception + */ + @Test + public void matchRoamingConsortium() throws Exception { + long[] providerRCOIs = new long[] {0x1234L, 0x2345L}; + Long[] anqpRCOIs = new Long[] {0x1234L, 0x2133L}; + + // Setup test provider. + PasspointConfiguration config = new PasspointConfiguration(); + config.homeSp = new HomeSP(); + config.homeSp.roamingConsortiumOIs = providerRCOIs; + config.credential = new Credential(); + config.credential.userCredential = new Credential.UserCredential(); + config.credential.userCredential.nonEapInnerMethod = "MS-CHAP-V2"; + mProvider = createProvider(config); + + // Setup Roaming Consortium ANQP element. + Map<ANQPElementType, ANQPElement> anqpElementMap = new HashMap<>(); + anqpElementMap.put(ANQPElementType.ANQPRoamingConsortium, + createRoamingConsortiumElement(anqpRCOIs)); + + assertEquals(PasspointMatch.RoamingProvider, mProvider.match(anqpElementMap)); + } + + /** + * Verify that a provider is a roaming provider when the provider's IMSI parameter and an + * IMSI from the SIM card matches a MCC-MNC in the 3GPP Network ANQP element. + * + * @throws Exception + */ + @Test + public void matchThreeGPPNetwork() throws Exception { + String testImsi = "1234567890"; + + // Setup test provider. + PasspointConfiguration config = new PasspointConfiguration(); + config.homeSp = new HomeSP(); + config.credential = new Credential(); + config.credential.simCredential = new Credential.SimCredential(); + config.credential.simCredential.imsi = testImsi; + when(mSimAccessor.getMatchingImsis(new IMSIParameter(testImsi, false))) + .thenReturn(Arrays.asList(new String[] {testImsi})); + mProvider = createProvider(config); + + // Setup 3GPP Network ANQP element. + Map<ANQPElementType, ANQPElement> anqpElementMap = new HashMap<>(); + anqpElementMap.put(ANQPElementType.ANQP3GPPNetwork, + createThreeGPPNetworkElement(new String[] {"123456"})); + + assertEquals(PasspointMatch.RoamingProvider, mProvider.match(anqpElementMap)); + } + + /** + * Verify that a provider is a roaming provider when its credential matches a NAI realm in + * the NAI Realm ANQP element. + * + * @throws Exception + */ + @Test + public void matchNAIRealm() throws Exception { + String testRealm = "realm.com"; + + // Setup test provider. + PasspointConfiguration config = new PasspointConfiguration(); + config.homeSp = new HomeSP(); + config.credential = new Credential(); + config.credential.realm = testRealm; + config.credential.userCredential = new Credential.UserCredential(); + config.credential.userCredential.nonEapInnerMethod = "MS-CHAP-V2"; + mProvider = createProvider(config); + + // Setup NAI Realm ANQP element. + Map<ANQPElementType, ANQPElement> anqpElementMap = new HashMap<>(); + anqpElementMap.put(ANQPElementType.ANQPNAIRealm, + createNAIRealmElement(testRealm, EAPConstants.EAP_TTLS, + new NonEAPInnerAuth(NonEAPInnerAuth.AUTH_TYPE_MSCHAPV2))); + + assertEquals(PasspointMatch.RoamingProvider, mProvider.match(anqpElementMap)); + } + + /** + * Verify that a provider is a home provider when its FQDN, roaming consortium OI, and + * IMSI all matched against the ANQP elements, since we prefer matching home provider over + * roaming provider. + * + * @throws Exception + */ + @Test + public void matchHomeOverRoamingProvider() throws Exception { + // Setup test data. + String testDomain = "test.com"; + String testImsi = "1234567890"; + long[] providerRCOIs = new long[] {0x1234L, 0x2345L}; + Long[] anqpRCOIs = new Long[] {0x1234L, 0x2133L}; + + // Setup test provider. + PasspointConfiguration config = new PasspointConfiguration(); + config.homeSp = new HomeSP(); + config.homeSp.fqdn = testDomain; + config.homeSp.roamingConsortiumOIs = providerRCOIs; + config.credential = new Credential(); + config.credential.simCredential = new Credential.SimCredential(); + config.credential.simCredential.imsi = testImsi; + when(mSimAccessor.getMatchingImsis(new IMSIParameter(testImsi, false))) + .thenReturn(Arrays.asList(new String[] {testImsi})); + mProvider = createProvider(config); + + // Setup ANQP elements. + Map<ANQPElementType, ANQPElement> anqpElementMap = new HashMap<>(); + anqpElementMap.put(ANQPElementType.ANQPDomName, + createDomainNameElement(new String[] {testDomain})); + anqpElementMap.put(ANQPElementType.ANQPRoamingConsortium, + createRoamingConsortiumElement(anqpRCOIs)); + anqpElementMap.put(ANQPElementType.ANQP3GPPNetwork, + createThreeGPPNetworkElement(new String[] {"123456"})); + + assertEquals(PasspointMatch.HomeProvider, mProvider.match(anqpElementMap)); + } } |