diff --git a/wifi/java/android/net/wifi/hotspot2/PasspointConfiguration.java b/wifi/java/android/net/wifi/hotspot2/PasspointConfiguration.java index 643753abf5dc5..ca4d121418719 100644 --- a/wifi/java/android/net/wifi/hotspot2/PasspointConfiguration.java +++ b/wifi/java/android/net/wifi/hotspot2/PasspointConfiguration.java @@ -18,9 +18,19 @@ package android.net.wifi.hotspot2; import android.net.wifi.hotspot2.pps.Credential; import android.net.wifi.hotspot2.pps.HomeSP; +import android.net.wifi.hotspot2.pps.Policy; +import android.net.wifi.hotspot2.pps.UpdateParameter; import android.os.Parcelable; +import android.text.TextUtils; +import android.util.Log; import android.os.Parcel; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + /** * Class representing Passpoint configuration. This contains configurations specified in * PerProviderSubscription (PPS) Management Object (MO) tree. @@ -28,13 +38,108 @@ import android.os.Parcel; * For more info, refer to Hotspot 2.0 PPS MO defined in section 9.1 of the Hotspot 2.0 * Release 2 Technical Specification. * - * Currently, only HomeSP and Credential subtrees are supported. - * * @hide */ public final class PasspointConfiguration implements Parcelable { + private static final String TAG = "PasspointConfiguration"; + + /** + * Number of bytes for certificate SHA-256 fingerprint byte array. + */ + private static final int CERTIFICATE_SHA256_BYTES = 32; + + /** + * Maximum bytes for URL string. + */ + private static final int MAX_URL_BYTES = 1023; + + /** + * Integer value used for indicating null value in the Parcel. + */ + private static final int NULL_VALUE = -1; + public HomeSP homeSp = null; public Credential credential = null; + public Policy policy = null; + + /** + * Meta data for performing subscription update. + */ + public UpdateParameter subscriptionUpdate = null; + + /** + * List of HTTPS URL for retrieving trust root certificate and the corresponding SHA-256 + * fingerprint of the certificate. The certificates are used for verifying AAA server's + * identity during EAP authentication. + */ + public Map trustRootCertList = null; + + /** + * Set by the subscription server, updated every time the configuration is updated by + * the subscription server. + * + * Use Integer.MIN_VALUE to indicate unset value. + */ + public int updateIdentifier = Integer.MIN_VALUE; + + /** + * The priority of the credential. + * + * Use Integer.MIN_VALUE to indicate unset value. + */ + public int credentialPriority = Integer.MIN_VALUE; + + /** + * The time this subscription is created. It is in the format of number + * of milliseconds since January 1, 1970, 00:00:00 GMT. + * + * Use Long.MIN_VALUE to indicate unset value. + */ + public long subscriptionCreationTimeInMs = Long.MIN_VALUE; + + /** + * The time this subscription will expire. It is in the format of number + * of milliseconds since January 1, 1970, 00:00:00 GMT. + * + * Use Long.MIN_VALUE to indicate unset value. + */ + public long subscriptionExpirationTimeInMs = Long.MIN_VALUE; + + /** + * The type of the subscription. This is defined by the provider and the value is provider + * specific. + */ + public String subscriptionType = null; + + /** + * The time period for usage statistics accumulation. A value of zero means that usage + * statistics are not accumulated on a periodic basis (e.g., a one-time limit for + * “pay as you go” - PAYG service). A non-zero value specifies the usage interval in minutes. + */ + public long usageLimitUsageTimePeriodInMinutes = Long.MIN_VALUE; + + /** + * The time at which usage statistic accumulation begins. It is in the format of number + * of milliseconds since January 1, 1970, 00:00:00 GMT. + * + * Use Long.MIN_VALUE to indicate unset value. + */ + public long usageLimitStartTimeInMs = Long.MIN_VALUE; + + /** + * The cumulative data limit in megabytes for the {@link #usageLimitUsageTimePeriodInMinutes}. + * A value of zero indicate unlimited data usage. + * + * Use Long.MIN_VALUE to indicate unset value. + */ + public long usageLimitDataLimit = Long.MIN_VALUE; + + /** + * The cumulative time limit in minutes for the {@link #usageLimitUsageTimePeriodInMinutes}. + * A value of zero indicate unlimited time usage. + */ + public long usageLimitTimeLimitInMinutes = Long.MIN_VALUE; + /** * Constructor for creating PasspointConfiguration with default values. @@ -47,14 +152,34 @@ public final class PasspointConfiguration implements Parcelable { * @param source The source to copy from */ public PasspointConfiguration(PasspointConfiguration source) { - if (source != null) { - if (source.homeSp != null) { - homeSp = new HomeSP(source.homeSp); - } - if (source.credential != null) { - credential = new Credential(source.credential); - } + if (source == null) { + return; } + + if (source.homeSp != null) { + homeSp = new HomeSP(source.homeSp); + } + if (source.credential != null) { + credential = new Credential(source.credential); + } + if (source.policy != null) { + policy = new Policy(source.policy); + } + if (source.trustRootCertList != null) { + trustRootCertList = Collections.unmodifiableMap(source.trustRootCertList); + } + if (source.subscriptionUpdate != null) { + subscriptionUpdate = new UpdateParameter(source.subscriptionUpdate); + } + updateIdentifier = source.updateIdentifier; + credentialPriority = source.credentialPriority; + subscriptionCreationTimeInMs = source.subscriptionCreationTimeInMs; + subscriptionExpirationTimeInMs = source.subscriptionExpirationTimeInMs; + subscriptionType = source.subscriptionType; + usageLimitDataLimit = source.usageLimitDataLimit; + usageLimitStartTimeInMs = source.usageLimitStartTimeInMs; + usageLimitTimeLimitInMinutes = source.usageLimitTimeLimitInMinutes; + usageLimitUsageTimePeriodInMinutes = source.usageLimitUsageTimePeriodInMinutes; } @Override @@ -66,6 +191,18 @@ public final class PasspointConfiguration implements Parcelable { public void writeToParcel(Parcel dest, int flags) { dest.writeParcelable(homeSp, flags); dest.writeParcelable(credential, flags); + dest.writeParcelable(policy, flags); + dest.writeParcelable(subscriptionUpdate, flags); + writeTrustRootCerts(dest, trustRootCertList); + dest.writeInt(updateIdentifier); + dest.writeInt(credentialPriority); + dest.writeLong(subscriptionCreationTimeInMs); + dest.writeLong(subscriptionExpirationTimeInMs); + dest.writeString(subscriptionType); + dest.writeLong(usageLimitUsageTimePeriodInMinutes); + dest.writeLong(usageLimitStartTimeInMs); + dest.writeLong(usageLimitDataLimit); + dest.writeLong(usageLimitTimeLimitInMinutes); } @Override @@ -77,9 +214,22 @@ public final class PasspointConfiguration implements Parcelable { return false; } PasspointConfiguration that = (PasspointConfiguration) thatObject; - return (homeSp == null ? that.homeSp == null : homeSp.equals(that.homeSp)) && - (credential == null ? that.credential == null : - credential.equals(that.credential)); + return (homeSp == null ? that.homeSp == null : homeSp.equals(that.homeSp)) + && (credential == null ? that.credential == null + : credential.equals(that.credential)) + && (policy == null) ? that.policy == null : policy.equals(that.policy) + && (subscriptionUpdate == null) ? that.subscriptionUpdate == null + : subscriptionUpdate.equals(that.subscriptionUpdate) + && isTrustRootCertListEquals(trustRootCertList, that.trustRootCertList) + && updateIdentifier == that.updateIdentifier + && credentialPriority == that.credentialPriority + && subscriptionCreationTimeInMs == that.subscriptionCreationTimeInMs + && subscriptionExpirationTimeInMs == that.subscriptionExpirationTimeInMs + && TextUtils.equals(subscriptionType, that.subscriptionType) + && usageLimitUsageTimePeriodInMinutes == that.usageLimitUsageTimePeriodInMinutes + && usageLimitStartTimeInMs == that.usageLimitStartTimeInMs + && usageLimitDataLimit == that.usageLimitDataLimit + && usageLimitTimeLimitInMinutes == that .usageLimitTimeLimitInMinutes; } /** @@ -94,6 +244,37 @@ public final class PasspointConfiguration implements Parcelable { if (credential == null || !credential.validate()) { return false; } + if (policy != null && !policy.validate()) { + return false; + } + if (subscriptionUpdate != null && !subscriptionUpdate.validate()) { + return false; + } + if (trustRootCertList != null) { + for (Map.Entry entry : trustRootCertList.entrySet()) { + String url = entry.getKey(); + byte[] certFingerprint = entry.getValue(); + if (TextUtils.isEmpty(url)) { + Log.d(TAG, "Empty URL"); + return false; + } + if (url.getBytes(StandardCharsets.UTF_8).length > MAX_URL_BYTES) { + Log.d(TAG, "URL bytes exceeded the max: " + + url.getBytes(StandardCharsets.UTF_8).length); + return false; + } + + if (certFingerprint == null) { + Log.d(TAG, "Fingerprint not specified"); + return false; + } + if (certFingerprint.length != CERTIFICATE_SHA256_BYTES) { + Log.d(TAG, "Incorrect size of trust root certificate SHA-256 fingerprint: " + + certFingerprint.length); + return false; + } + } + } return true; } @@ -104,11 +285,88 @@ public final class PasspointConfiguration implements Parcelable { PasspointConfiguration config = new PasspointConfiguration(); config.homeSp = in.readParcelable(null); config.credential = in.readParcelable(null); + config.policy = in.readParcelable(null); + config.subscriptionUpdate = in.readParcelable(null); + config.trustRootCertList = readTrustRootCerts(in); + config.updateIdentifier = in.readInt(); + config.credentialPriority = in.readInt(); + config.subscriptionCreationTimeInMs = in.readLong(); + config.subscriptionExpirationTimeInMs = in.readLong(); + config.subscriptionType = in.readString(); + config.usageLimitUsageTimePeriodInMinutes = in.readLong(); + config.usageLimitStartTimeInMs = in.readLong(); + config.usageLimitDataLimit = in.readLong(); + config.usageLimitTimeLimitInMinutes = in.readLong(); return config; } + @Override public PasspointConfiguration[] newArray(int size) { return new PasspointConfiguration[size]; } + + /** + * Helper function for reading trust root certificate info list from a Parcel. + * + * @param in The Parcel to read from + * @return The list of trust root certificate URL with the corresponding certificate + * fingerprint + */ + private Map readTrustRootCerts(Parcel in) { + int size = in.readInt(); + if (size == NULL_VALUE) { + return null; + } + Map trustRootCerts = new HashMap<>(size); + for (int i = 0; i < size; i++) { + String key = in.readString(); + byte[] value = in.createByteArray(); + trustRootCerts.put(key, value); + } + return trustRootCerts; + } }; + + /** + * Helper function for writing trust root certificate information list. + * + * @param dest The Parcel to write to + * @param trustRootCerts The list of trust root certificate URL with the corresponding + * certificate fingerprint + */ + private static void writeTrustRootCerts(Parcel dest, Map trustRootCerts) { + if (trustRootCerts == null) { + dest.writeInt(NULL_VALUE); + return; + } + dest.writeInt(trustRootCerts.size()); + for (Map.Entry entry : trustRootCerts.entrySet()) { + dest.writeString(entry.getKey()); + dest.writeByteArray(entry.getValue()); + } + } + + /** + * Helper function for comparing two trust root certificate list. Cannot use Map#equals + * method since the value type (byte[]) doesn't override equals method. + * + * @param list1 The first trust root certificate list + * @param list2 The second trust root certificate list + * @return true if the two list are equal + */ + private static boolean isTrustRootCertListEquals(Map list1, + Map list2) { + if (list1 == null || list2 == null) { + return list1 == list2; + } + if (list1.size() != list2.size()) { + return false; + } + for (Map.Entry entry : list1.entrySet()) { + if (!Arrays.equals(entry.getValue(), list2.get(entry.getKey()))) { + return false; + } + } + return true; + } } diff --git a/wifi/java/android/net/wifi/hotspot2/omadm/PPSMOParser.java b/wifi/java/android/net/wifi/hotspot2/omadm/PPSMOParser.java index 98fd0f3bde15f..22b0f977d3e86 100644 --- a/wifi/java/android/net/wifi/hotspot2/omadm/PPSMOParser.java +++ b/wifi/java/android/net/wifi/hotspot2/omadm/PPSMOParser.java @@ -19,6 +19,8 @@ package android.net.wifi.hotspot2.omadm; import android.net.wifi.hotspot2.PasspointConfiguration; import android.net.wifi.hotspot2.pps.Credential; import android.net.wifi.hotspot2.pps.HomeSP; +import android.net.wifi.hotspot2.pps.Policy; +import android.net.wifi.hotspot2.pps.UpdateParameter; import android.text.TextUtils; import android.util.Log; import android.util.Pair; @@ -130,6 +132,20 @@ public final class PPSMOParser { */ private static final String NODE_PER_PROVIDER_SUBSCRIPTION = "PerProviderSubscription"; + /** + * Fields under PerProviderSubscription. + */ + private static final String NODE_UPDATE_IDENTIFIER = "UpdateIdentifier"; + private static final String NODE_AAA_SERVER_TRUST_ROOT = "AAAServerTrustRoot"; + private static final String NODE_SUBSCRIPTION_UPDATE = "SubscriptionUpdate"; + private static final String NODE_SUBSCRIPTION_PARAMETER = "SubscriptionParameter"; + private static final String NODE_TYPE_OF_SUBSCRIPTION = "TypeOfSubscription"; + private static final String NODE_USAGE_LIMITS = "UsageLimits"; + private static final String NODE_DATA_LIMIT = "DataLimit"; + private static final String NODE_START_DATE = "StartDate"; + private static final String NODE_TIME_LIMIT = "TimeLimit"; + private static final String NODE_USAGE_TIME_PERIOD = "UsageTimePeriod"; + private static final String NODE_CREDENTIAL_PRIORITY = "CredentialPriority"; /** * Fields under HomeSP subtree. */ @@ -168,12 +184,39 @@ public final class PPSMOParser { private static final String NODE_INNER_METHOD = "InnerMethod"; private static final String NODE_DIGITAL_CERTIFICATE = "DigitalCertificate"; private static final String NODE_CERTIFICATE_TYPE = "CertificateType"; - private static final String NODE_CERT_SHA256_FINGERPRINT = "CertSHA256FingerPrint"; + private static final String NODE_CERT_SHA256_FINGERPRINT = "CertSHA256Fingerprint"; private static final String NODE_REALM = "Realm"; private static final String NODE_SIM = "SIM"; private static final String NODE_SIM_IMSI = "IMSI"; private static final String NODE_CHECK_AAA_SERVER_CERT_STATUS = "CheckAAAServerCertStatus"; + /** + * Fields under Policy subtree. + */ + private static final String NODE_POLICY = "Policy"; + private static final String NODE_PREFERRED_ROAMING_PARTNER_LIST = + "PreferredRoamingPartnerList"; + private static final String NODE_FQDN_MATCH = "FQDN_Match"; + private static final String NODE_PRIORITY = "Priority"; + private static final String NODE_COUNTRY = "Country"; + private static final String NODE_MIN_BACKHAUL_THRESHOLD = "MinBackhaulThreshold"; + private static final String NODE_NETWORK_TYPE = "NetworkType"; + private static final String NODE_DOWNLINK_BANDWIDTH = "DLBandwidth"; + private static final String NODE_UPLINK_BANDWIDTH = "ULBandwidth"; + private static final String NODE_POLICY_UPDATE = "PolicyUpdate"; + private static final String NODE_UPDATE_INTERVAL = "UpdateInterval"; + private static final String NODE_UPDATE_METHOD = "UpdateMethod"; + private static final String NODE_RESTRICTION = "Restriction"; + private static final String NODE_URI = "URI"; + private static final String NODE_TRUST_ROOT = "TrustRoot"; + private static final String NODE_CERT_URL = "CertURL"; + private static final String NODE_SP_EXCLUSION_LIST = "SPExclusionList"; + private static final String NODE_REQUIRED_PROTO_PORT_TUPLE = "RequiredProtoPortTuple"; + private static final String NODE_IP_PROTOCOL = "IPProtocol"; + private static final String NODE_PORT_NUMBER = "PortNumber"; + private static final String NODE_MAXIMUM_BSS_LOAD_VALUE = "MaximumBSSLoadValue"; + private static final String NODE_OTHER = "Other"; + /** * URN (Unique Resource Name) for PerProviderSubscription Management Object Tree. */ @@ -349,6 +392,10 @@ public final class PPSMOParser { * ... * * + * UpdateIdentifier + * ... + * + * * ... * * @@ -361,11 +408,12 @@ public final class PPSMOParser { throws ParsingException { PasspointConfiguration config = null; String nodeName = null; + int updateIdentifier = Integer.MIN_VALUE; for (XMLNode child : node.getChildren()) { switch (child.getTag()) { case TAG_NODE_NAME: if (nodeName != null) { - throw new ParsingException("Duplicant NodeName: " + child.getText()); + throw new ParsingException("Duplicate NodeName: " + child.getText()); } nodeName = child.getText(); if (!TextUtils.equals(nodeName, NODE_PER_PROVIDER_SUBSCRIPTION)) { @@ -373,13 +421,22 @@ public final class PPSMOParser { } break; case TAG_NODE: - // Only one PerProviderSubscription instance is expected and allowed. - if (config != null) { - throw new ParsingException("Multiple PPS instance"); + // A node can be either an UpdateIdentifier node or a PerProviderSubscription + // instance node. Flatten out the XML tree first by converting it to a PPS + // tree to reduce the complexity of the parsing code. + PPSNode ppsNodeRoot = buildPpsNode(child); + if (TextUtils.equals(ppsNodeRoot.getName(), NODE_UPDATE_IDENTIFIER)) { + if (updateIdentifier != Integer.MIN_VALUE) { + throw new ParsingException("Multiple node for UpdateIdentifier"); + } + updateIdentifier = parseInteger(getPpsNodeValue(ppsNodeRoot)); + } else { + // Only one PerProviderSubscription instance is expected and allowed. + if (config != null) { + throw new ParsingException("Multiple PPS instance"); + } + config = parsePpsInstance(ppsNodeRoot); } - // Convert the XML tree to a PPS tree. - PPSNode ppsInstanceRoot = buildPpsNode(child); - config = parsePpsInstance(ppsInstanceRoot); break; case TAG_RT_PROPERTIES: // Parse and verify URN stored in the RT (Run Time) Properties. @@ -392,6 +449,9 @@ public final class PPSMOParser { throw new ParsingException("Unknown tag under PPS node: " + child.getTag()); } } + if (config != null && updateIdentifier != Integer.MIN_VALUE) { + config.updateIdentifier = updateIdentifier; + } return config; } @@ -551,6 +611,21 @@ public final class PPSMOParser { case NODE_CREDENTIAL: config.credential = parseCredential(child); break; + case NODE_POLICY: + config.policy = parsePolicy(child); + break; + case NODE_AAA_SERVER_TRUST_ROOT: + config.trustRootCertList = parseAAAServerTrustRootList(child); + break; + case NODE_SUBSCRIPTION_UPDATE: + config.subscriptionUpdate = parseUpdateParameter(child); + break; + case NODE_SUBSCRIPTION_PARAMETER: + parseSubscriptionParameter(child, config); + break; + case NODE_CREDENTIAL_PRIORITY: + config.credentialPriority = parseInteger(getPpsNodeValue(child)); + break; default: throw new ParsingException("Unknown node: " + child.getName()); } @@ -616,11 +691,7 @@ public final class PPSMOParser { String[] oiStrArray = oiStr.split(","); long[] oiArray = new long[oiStrArray.length]; for (int i = 0; i < oiStrArray.length; i++) { - try { - oiArray[i] = Long.parseLong(oiStrArray[i], 16); - } catch (NumberFormatException e) { - throw new ParsingException("Invalid OI: " + oiStrArray[i]); - } + oiArray[i] = parseLong(oiStrArray[i], 16); } return oiArray; } @@ -671,11 +742,7 @@ public final class PPSMOParser { ssid = getPpsNodeValue(child); break; case NODE_HESSID: - try { - hessid = Long.parseLong(getPpsNodeValue(child), 16); - } catch (NumberFormatException e) { - throw new ParsingException("Invalid HESSID: " + getPpsNodeValue(child)); - } + hessid = parseLong(getPpsNodeValue(child), 16); break; default: throw new ParsingException("Unknown node under NetworkID instance: " + @@ -998,6 +1065,503 @@ public final class PPSMOParser { return simCred; } + /** + * Parse configurations under PerProviderSubscription/Policy subtree. + * + * @param node PPSNode representing the root of the PerProviderSubscription/Policy subtree + * @return {@link Policy} + * @throws ParsingException + */ + private static Policy parsePolicy(PPSNode node) throws ParsingException { + if (node.isLeaf()) { + throw new ParsingException("Leaf node not expected for Policy"); + } + + Policy policy = new Policy(); + for (PPSNode child : node.getChildren()) { + switch (child.getName()) { + case NODE_PREFERRED_ROAMING_PARTNER_LIST: + policy.preferredRoamingPartnerList = parsePreferredRoamingPartnerList(child); + break; + case NODE_MIN_BACKHAUL_THRESHOLD: + parseMinBackhaulThreshold(child, policy); + break; + case NODE_POLICY_UPDATE: + policy.policyUpdate = parseUpdateParameter(child); + break; + case NODE_SP_EXCLUSION_LIST: + policy.excludedSsidList = parseSpExclusionList(child); + break; + case NODE_REQUIRED_PROTO_PORT_TUPLE: + policy.requiredProtoPortMap = parseRequiredProtoPortTuple(child); + break; + case NODE_MAXIMUM_BSS_LOAD_VALUE: + policy.maximumBssLoadValue = parseInteger(getPpsNodeValue(child)); + break; + default: + throw new ParsingException("Unknown node under Policy: " + child.getName()); + } + } + return policy; + } + + /** + * Parse configurations under PerProviderSubscription/Policy/PreferredRoamingPartnerList + * subtree. + * + * @param node PPSNode representing the root of the + * PerProviderSubscription/Policy/PreferredRoamingPartnerList subtree + * @return List of {@link Policy#RoamingPartner} + * @throws ParsingException + */ + private static List parsePreferredRoamingPartnerList(PPSNode node) + throws ParsingException { + if (node.isLeaf()) { + throw new ParsingException("Leaf node not expected for PreferredRoamingPartnerList"); + } + List partnerList = new ArrayList<>(); + for (PPSNode child : node.getChildren()) { + partnerList.add(parsePreferredRoamingPartner(child)); + } + return partnerList; + } + + /** + * Parse configurations under PerProviderSubscription/Policy/PreferredRoamingPartnerList/ + * subtree. + * + * @param node PPSNode representing the root of the + * PerProviderSubscription/Policy/PreferredRoamingPartnerList/ subtree + * @return {@link Policy#RoamingPartner} + * @throws ParsingException + */ + private static Policy.RoamingPartner parsePreferredRoamingPartner(PPSNode node) + throws ParsingException { + if (node.isLeaf()) { + throw new ParsingException("Leaf node not expected for PreferredRoamingPartner " + + "instance"); + } + + Policy.RoamingPartner roamingPartner = new Policy.RoamingPartner(); + for (PPSNode child : node.getChildren()) { + switch (child.getName()) { + case NODE_FQDN_MATCH: + // FQDN_Match field is in the format of "[FQDN],[MatchInfo]", where [MatchInfo] + // is either "exactMatch" for exact match of FQDN or "includeSubdomains" for + // matching all FQDNs with the same sub-domain. + String fqdnMatch = getPpsNodeValue(child); + String[] fqdnMatchArray = fqdnMatch.split(","); + if (fqdnMatchArray.length != 2) { + throw new ParsingException("Invalid FQDN_Match: " + fqdnMatch); + } + roamingPartner.fqdn = fqdnMatchArray[0]; + if (TextUtils.equals(fqdnMatchArray[1], "exactMatch")) { + roamingPartner.fqdnExactMatch = true; + } else if (TextUtils.equals(fqdnMatchArray[1], "includeSubdomains")) { + roamingPartner.fqdnExactMatch = false; + } else { + throw new ParsingException("Invalid FQDN_Match: " + fqdnMatch); + } + break; + case NODE_PRIORITY: + roamingPartner.priority = parseInteger(getPpsNodeValue(child)); + break; + case NODE_COUNTRY: + roamingPartner.countries = getPpsNodeValue(child); + break; + default: + throw new ParsingException("Unknown node under PreferredRoamingPartnerList " + + "instance " + child.getName()); + } + } + return roamingPartner; + } + + /** + * Parse configurations under PerProviderSubscription/Policy/MinBackhaulThreshold subtree + * into the given policy. + * + * @param node PPSNode representing the root of the + * PerProviderSubscription/Policy/MinBackhaulThreshold subtree + * @param policy The policy to store the MinBackhualThreshold configuration + * @throws ParsingException + */ + private static void parseMinBackhaulThreshold(PPSNode node, Policy policy) + throws ParsingException { + if (node.isLeaf()) { + throw new ParsingException("Leaf node not expected for MinBackhaulThreshold"); + } + for (PPSNode child : node.getChildren()) { + parseMinBackhaulThresholdInstance(child, policy); + } + } + + /** + * Parse configurations under PerProviderSubscription/Policy/MinBackhaulThreshold/ subtree + * into the given policy. + * + * @param node PPSNode representing the root of the + * PerProviderSubscription/Policy/MinBackhaulThreshold/ subtree + * @param policy The policy to store the MinBackhaulThreshold configuration + * @throws ParsingException + */ + private static void parseMinBackhaulThresholdInstance(PPSNode node, Policy policy) + throws ParsingException { + if (node.isLeaf()) { + throw new ParsingException("Leaf node not expected for MinBackhaulThreshold instance"); + } + String networkType = null; + long downlinkBandwidth = Long.MIN_VALUE; + long uplinkBandwidth = Long.MIN_VALUE; + for (PPSNode child : node.getChildren()) { + switch (child.getName()) { + case NODE_NETWORK_TYPE: + networkType = getPpsNodeValue(child); + break; + case NODE_DOWNLINK_BANDWIDTH: + downlinkBandwidth = parseLong(getPpsNodeValue(child), 10); + break; + case NODE_UPLINK_BANDWIDTH: + uplinkBandwidth = parseLong(getPpsNodeValue(child), 10); + break; + default: + throw new ParsingException("Unknown node under MinBackhaulThreshold instance " + + child.getName()); + } + } + if (networkType == null) { + throw new ParsingException("Missing NetworkType field"); + } + + if (TextUtils.equals(networkType, "home")) { + policy.minHomeDownlinkBandwidth = downlinkBandwidth; + policy.minHomeUplinkBandwidth = uplinkBandwidth; + } else if (TextUtils.equals(networkType, "roaming")) { + policy.minRoamingDownlinkBandwidth = downlinkBandwidth; + policy.minRoamingUplinkBandwidth = uplinkBandwidth; + } else { + throw new ParsingException("Invalid network type: " + networkType); + } + } + + /** + * Parse update parameters. This contained configurations from either + * PerProviderSubscription/Policy/PolicyUpdate or PerProviderSubscription/SubscriptionUpdate + * subtree. + * + * @param node PPSNode representing the root of the PerProviderSubscription/Policy/PolicyUpdate + * or PerProviderSubscription/SubscriptionUpdate subtree + * @return {@link UpdateParameter} + * @throws ParsingException + */ + private static UpdateParameter parseUpdateParameter(PPSNode node) + throws ParsingException { + if (node.isLeaf()) { + throw new ParsingException("Leaf node not expected for Update Parameters"); + } + + UpdateParameter updateParam = new UpdateParameter(); + for (PPSNode child : node.getChildren()) { + switch(child.getName()) { + case NODE_UPDATE_INTERVAL: + updateParam.updateIntervalInMinutes = parseLong(getPpsNodeValue(child), 10); + break; + case NODE_UPDATE_METHOD: + updateParam.updateMethod = getPpsNodeValue(child); + break; + case NODE_RESTRICTION: + updateParam.restriction = getPpsNodeValue(child); + break; + case NODE_URI: + updateParam.serverUri = getPpsNodeValue(child); + break; + case NODE_USERNAME_PASSWORD: + Pair usernamePassword = parseUpdateUserCredential(child); + updateParam.username = usernamePassword.first; + updateParam.base64EncodedPassword = usernamePassword.second; + break; + case NODE_TRUST_ROOT: + Pair trustRoot = parseTrustRoot(child); + updateParam.trustRootCertUrl = trustRoot.first; + updateParam.trustRootCertSha256Fingerprint = trustRoot.second; + break; + case NODE_OTHER: + Log.d(TAG, "Ignore unsupported paramter: " + child.getName()); + break; + default: + throw new ParsingException("Unknown node under Update Parameters: " + + child.getName()); + } + } + return updateParam; + } + + /** + * Parse username and password parameters associated with policy or subscription update. + * This contained configurations under either + * PerProviderSubscription/Policy/PolicyUpdate/UsernamePassword or + * PerProviderSubscription/SubscriptionUpdate/UsernamePassword subtree. + * + * @param node PPSNode representing the root of the UsernamePassword subtree + * @return Pair of username and password + * @throws ParsingException + */ + private static Pair parseUpdateUserCredential(PPSNode node) + throws ParsingException { + if (node.isLeaf()) { + throw new ParsingException("Leaf node not expected for UsernamePassword"); + } + + String username = null; + String password = null; + for (PPSNode child : node.getChildren()) { + switch (child.getName()) { + case NODE_USERNAME: + username = getPpsNodeValue(child); + break; + case NODE_PASSWORD: + password = getPpsNodeValue(child); + break; + default: + throw new ParsingException("Unknown node under UsernamePassword: " + + child.getName()); + } + } + return Pair.create(username, password); + } + + /** + * Parse the trust root parameters associated with policy update, subscription update, or AAA + * server trust root. + * + * This contained configurations under either + * PerProviderSubscription/Policy/PolicyUpdate/TrustRoot or + * PerProviderSubscription/SubscriptionUpdate/TrustRoot or + * PerProviderSubscription/AAAServerTrustRoot/ subtree. + * + * @param node PPSNode representing the root of the TrustRoot subtree + * @return Pair of Certificate URL and fingerprint + * @throws ParsingException + */ + private static Pair parseTrustRoot(PPSNode node) + throws ParsingException { + if (node.isLeaf()) { + throw new ParsingException("Leaf node not expected for TrustRoot"); + } + + String certUrl = null; + byte[] certFingerprint = null; + for (PPSNode child : node.getChildren()) { + switch (child.getName()) { + case NODE_CERT_URL: + certUrl = getPpsNodeValue(child); + break; + case NODE_CERT_SHA256_FINGERPRINT: + certFingerprint = parseHexString(getPpsNodeValue(child)); + break; + default: + throw new ParsingException("Unknown node under TrustRoot: " + + child.getName()); + } + } + return Pair.create(certUrl, certFingerprint); + } + + /** + * Parse configurations under PerProviderSubscription/Policy/SPExclusionList subtree. + * + * @param node PPSNode representing the root of the + * PerProviderSubscription/Policy/SPExclusionList subtree + * @return Array of excluded SSIDs + * @throws ParsingException + */ + private static String[] parseSpExclusionList(PPSNode node) throws ParsingException { + if (node.isLeaf()) { + throw new ParsingException("Leaf node not expected for SPExclusionList"); + } + List ssidList = new ArrayList<>(); + for (PPSNode child : node.getChildren()) { + ssidList.add(parseSpExclusionInstance(child)); + } + return ssidList.toArray(new String[ssidList.size()]); + } + + /** + * Parse configurations under PerProviderSubscription/Policy/SPExclusionList/ subtree. + * + * @param node PPSNode representing the root of the + * PerProviderSubscription/Policy/SPExclusionList/ subtree + * @return String + * @throws ParsingException + */ + private static String parseSpExclusionInstance(PPSNode node) throws ParsingException { + if (node.isLeaf()) { + throw new ParsingException("Leaf node not expected for SPExclusion instance"); + } + String ssid = null; + for (PPSNode child : node.getChildren()) { + switch (child.getName()) { + case NODE_SSID: + ssid = getPpsNodeValue(child); + break; + default: + throw new ParsingException("Unknown node under SPExclusion instance"); + } + } + return ssid; + } + + /** + * Parse configurations under PerProviderSubscription/Policy/RequiredProtoPortTuple subtree. + * + * @param node PPSNode representing the root of the + * PerProviderSubscription/Policy/RequiredProtoPortTuple subtree + * @return Map of IP Protocol to Port Number tuples + * @throws ParsingException + */ + private static Map parseRequiredProtoPortTuple(PPSNode node) + throws ParsingException { + if (node.isLeaf()) { + throw new ParsingException("Leaf node not expected for RequiredProtoPortTuple"); + } + Map protoPortTupleMap = new HashMap<>(); + for (PPSNode child : node.getChildren()) { + Pair protoPortTuple = parseProtoPortTuple(child); + protoPortTupleMap.put(protoPortTuple.first, protoPortTuple.second); + } + return protoPortTupleMap; + } + + /** + * Parse configurations under PerProviderSubscription/Policy/RequiredProtoPortTuple/ + * subtree. + * + * @param node PPSNode representing the root of the + * PerProviderSubscription/Policy/RequiredProtoPortTuple/ subtree + * @return Pair of IP Protocol to Port Number tuple + * @throws ParsingException + */ + private static Pair parseProtoPortTuple(PPSNode node) + throws ParsingException { + if (node.isLeaf()) { + throw new ParsingException("Leaf node not expected for RequiredProtoPortTuple " + + "instance"); + } + int proto = Integer.MIN_VALUE; + String ports = null; + for (PPSNode child : node.getChildren()) { + switch (child.getName()) { + case NODE_IP_PROTOCOL: + proto = parseInteger(getPpsNodeValue(child)); + break; + case NODE_PORT_NUMBER: + ports = getPpsNodeValue(child); + break; + default: + throw new ParsingException("Unknown node under RequiredProtoPortTuple instance" + + child.getName()); + } + } + if (proto == Integer.MIN_VALUE) { + throw new ParsingException("Missing IPProtocol field"); + } + if (ports == null) { + throw new ParsingException("Missing PortNumber field"); + } + return Pair.create(proto, ports); + } + + /** + * Parse configurations under PerProviderSubscription/AAAServerTrustRoot subtree. + * + * @param node PPSNode representing the root of PerProviderSubscription/AAAServerTrustRoot + * subtree + * @return Map of certificate URL with the corresponding certificate fingerprint + * @throws ParsingException + */ + private static Map parseAAAServerTrustRootList(PPSNode node) + throws ParsingException { + if (node.isLeaf()) { + throw new ParsingException("Leaf node not expected for AAAServerTrustRoot"); + } + Map certList = new HashMap<>(); + for (PPSNode child : node.getChildren()) { + Pair certTuple = parseTrustRoot(child); + certList.put(certTuple.first, certTuple.second); + } + return certList; + } + + /** + * Parse configurations under PerProviderSubscription/SubscriptionParameter subtree. + * + * @param node PPSNode representing the root of PerProviderSubscription/SubscriptionParameter + * subtree + * @param config Instance of {@link PasspointConfiguration} + * @throws ParsingException + */ + private static void parseSubscriptionParameter(PPSNode node, PasspointConfiguration config) + throws ParsingException { + if (node.isLeaf()) { + throw new ParsingException("Leaf node not expected for SubscriptionParameter"); + } + for (PPSNode child : node.getChildren()) { + switch (child.getName()) { + case NODE_CREATION_DATE: + config.subscriptionCreationTimeInMs = parseDate(getPpsNodeValue(child)); + break; + case NODE_EXPIRATION_DATE: + config.subscriptionExpirationTimeInMs = parseDate(getPpsNodeValue(child)); + break; + case NODE_TYPE_OF_SUBSCRIPTION: + config.subscriptionType = getPpsNodeValue(child); + break; + case NODE_USAGE_LIMITS: + parseUsageLimits(child, config); + break; + default: + throw new ParsingException("Unknown node under SubscriptionParameter" + + child.getName()); + } + } + } + + /** + * Parse configurations under PerProviderSubscription/SubscriptionParameter/UsageLimits + * subtree. + * + * @param node PPSNode representing the root of + * PerProviderSubscription/SubscriptionParameter/UsageLimits subtree + * @param config Instance of {@link PasspointConfiguration} + * @throws ParsingException + */ + private static void parseUsageLimits(PPSNode node, PasspointConfiguration config) + throws ParsingException { + if (node.isLeaf()) { + throw new ParsingException("Leaf node not expected for UsageLimits"); + } + for (PPSNode child : node.getChildren()) { + switch (child.getName()) { + case NODE_DATA_LIMIT: + config.usageLimitDataLimit = parseLong(getPpsNodeValue(child), 10); + break; + case NODE_START_DATE: + config.usageLimitStartTimeInMs = parseDate(getPpsNodeValue(child)); + break; + case NODE_TIME_LIMIT: + config.usageLimitTimeLimitInMinutes = parseLong(getPpsNodeValue(child), 10); + break; + case NODE_USAGE_TIME_PERIOD: + config.usageLimitUsageTimePeriodInMinutes = + parseLong(getPpsNodeValue(child), 10); + break; + default: + throw new ParsingException("Unknown node under UsageLimits" + + child.getName()); + } + } + } + /** * Convert a hex string to a byte array. * @@ -1053,6 +1617,21 @@ public final class PPSMOParser { } } + /** + * Parse a string representing a long integer. + * + * @param value String of long integer value + * @return long + * @throws ParsingException + */ + private static long parseLong(String value, int radix) throws ParsingException { + try { + return Long.parseLong(value, radix); + } catch (NumberFormatException e) { + throw new ParsingException("Invalid long integer value: " + value); + } + } + /** * Convert a List to a primitive long array long[]. * diff --git a/wifi/java/android/net/wifi/hotspot2/pps/Policy.aidl b/wifi/java/android/net/wifi/hotspot2/pps/Policy.aidl new file mode 100644 index 0000000000000..e923f1f0fee8d --- /dev/null +++ b/wifi/java/android/net/wifi/hotspot2/pps/Policy.aidl @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2017, 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 android.net.wifi.hotspot2.pps; + +parcelable Policy; diff --git a/wifi/java/android/net/wifi/hotspot2/pps/Policy.java b/wifi/java/android/net/wifi/hotspot2/pps/Policy.java new file mode 100644 index 0000000000000..b2583d324420c --- /dev/null +++ b/wifi/java/android/net/wifi/hotspot2/pps/Policy.java @@ -0,0 +1,452 @@ +/** + * Copyright (c) 2017, 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 android.net.wifi.hotspot2.pps; + +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; +import android.util.Log; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Class representing Policy subtree in PerProviderSubscription (PPS) + * Management Object (MO) tree. + * + * The Policy specifies additional criteria for Passpoint network selections, such as preferred + * roaming partner, minimum backhaul bandwidth, and etc. It also provides the meta data for + * updating the policy. + * + * For more info, refer to Hotspot 2.0 PPS MO defined in section 9.1 of the Hotspot 2.0 + * Release 2 Technical Specification. + * + * @hide + */ +public final class Policy implements Parcelable { + private static final String TAG = "Policy"; + + /** + * Default priority for preferred roaming partner. + */ + public static final int PREFERRED_ROAMING_PARTNER_DEFAULT_PRIORITY = 128; + + /** + * Maximum number of SSIDs in the exclusion list. + */ + private static final int MAX_EXCLUSION_SSIDS = 128; + + /** + * Maximum byte for SSID. + */ + private static final int MAX_SSID_BYTES = 32; + + /** + * Maximum bytes for port string in {@link #requiredProtoPortMap}. + */ + private static final int MAX_PORT_STRING_BYTES = 64; + + /** + * Integer value used for indicating null value in the Parcel. + */ + private static final int NULL_VALUE = -1; + + /** + * Minimum available downlink/uplink bandwidth (in kilobits per second) required when + * selecting a network from home providers. + * + * The bandwidth is calculated as the LinkSpeed * (1 – LinkLoad/255), where LinkSpeed + * and LinkLoad parameters are drawn from the WAN Metrics ANQP element at that hotspot. + * + * Using Long.MIN_VALUE to indicate unset value. + */ + public long minHomeDownlinkBandwidth = Long.MIN_VALUE; + public long minHomeUplinkBandwidth = Long.MIN_VALUE; + + /** + * Minimum available downlink/uplink bandwidth (in kilobits per second) required when + * selecting a network from roaming providers. + * + * The bandwidth is calculated as the LinkSpeed * (1 – LinkLoad/255), where LinkSpeed + * and LinkLoad parameters are drawn from the WAN Metrics ANQP element at that hotspot. + * + * Using Long.MIN_VALUE to indicate unset value. + */ + public long minRoamingDownlinkBandwidth = Long.MIN_VALUE; + public long minRoamingUplinkBandwidth = Long.MIN_VALUE; + + /** + * List of SSIDs that are not preferred by the Home SP. + */ + public String[] excludedSsidList = null; + + /** + * List of IP protocol and port number required by one or more operator supported application. + * The port string contained one or more port numbers delimited by ",". + */ + public Map requiredProtoPortMap = null; + + /** + * This specifies the maximum acceptable BSS load policy. This is used to prevent device + * from joining an AP whose channel is overly congested with traffic. + * Using Integer.MIN_VALUE to indicate unset value. + */ + public int maximumBssLoadValue = Integer.MIN_VALUE; + + /** + * Policy associated with a roaming provider. This specifies a priority associated + * with a roaming provider for given list of countries. + * + * Contains field under PerProviderSubscription/Policy/PreferredRoamingPartnerList. + */ + public static final class RoamingPartner implements Parcelable { + /** + * FQDN of the roaming partner. + */ + public String fqdn = null; + + /** + * Flag indicating the exact match of FQDN is required for FQDN matching. + * + * When this flag is set to false, sub-domain matching is used. For example, when + * {@link #fqdn} s set to "example.com", "host.example.com" would be a match. + */ + public boolean fqdnExactMatch = false; + + /** + * Priority associated with this roaming partner policy. + */ + public int priority = PREFERRED_ROAMING_PARTNER_DEFAULT_PRIORITY; + + /** + * A string contained One or more, comma delimited (i.e., ",") ISO/IEC 3166-1 two + * character country strings or the country-independent value, "*". + */ + public String countries = null; + + public RoamingPartner() {} + + public RoamingPartner(RoamingPartner source) { + if (source != null) { + fqdn = source.fqdn; + fqdnExactMatch = source.fqdnExactMatch; + priority = source.priority; + countries = source.countries; + } + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(fqdn); + dest.writeInt(fqdnExactMatch ? 1 : 0); + dest.writeInt(priority); + dest.writeString(countries); + } + + @Override + public boolean equals(Object thatObject) { + if (this == thatObject) { + return true; + } + if (!(thatObject instanceof RoamingPartner)) { + return false; + } + + RoamingPartner that = (RoamingPartner) thatObject; + return TextUtils.equals(fqdn, that.fqdn) + && fqdnExactMatch == that.fqdnExactMatch + && priority == that.priority + && TextUtils.equals(countries, that.countries); + } + + /** + * Validate RoamingParnter data. + * + * @return true on success + */ + public boolean validate() { + if (TextUtils.isEmpty(fqdn)) { + Log.d(TAG, "Missing FQDN"); + return false; + } + if (TextUtils.isEmpty(countries)) { + Log.d(TAG, "Missing countries"); + return false; + } + return true; + } + + public static final Creator CREATOR = + new Creator() { + @Override + public RoamingPartner createFromParcel(Parcel in) { + RoamingPartner roamingPartner = new RoamingPartner(); + roamingPartner.fqdn = in.readString(); + roamingPartner.fqdnExactMatch = in.readInt() != 0; + roamingPartner.priority = in.readInt(); + roamingPartner.countries = in.readString(); + return roamingPartner; + } + + @Override + public RoamingPartner[] newArray(int size) { + return new RoamingPartner[size]; + } + }; + } + public List preferredRoamingPartnerList = null; + + /** + * Meta data used for policy update. + */ + public UpdateParameter policyUpdate = null; + + /** + * Constructor for creating Policy with default values. + */ + public Policy() {} + + /** + * Copy constructor. + * + * @param source The source to copy from + */ + public Policy(Policy source) { + if (source == null) { + return; + } + minHomeDownlinkBandwidth = source.minHomeDownlinkBandwidth; + minHomeUplinkBandwidth = source.minHomeUplinkBandwidth; + minRoamingDownlinkBandwidth = source.minRoamingDownlinkBandwidth; + minRoamingUplinkBandwidth = source.minRoamingUplinkBandwidth; + maximumBssLoadValue = source.maximumBssLoadValue; + if (source.excludedSsidList != null) { + excludedSsidList = Arrays.copyOf(source.excludedSsidList, + source.excludedSsidList.length); + } + if (source.requiredProtoPortMap != null) { + requiredProtoPortMap = Collections.unmodifiableMap(source.requiredProtoPortMap); + } + if (source.preferredRoamingPartnerList != null) { + preferredRoamingPartnerList = Collections.unmodifiableList( + source.preferredRoamingPartnerList); + } + if (source.policyUpdate != null) { + policyUpdate = new UpdateParameter(source.policyUpdate); + } + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeLong(minHomeDownlinkBandwidth); + dest.writeLong(minHomeUplinkBandwidth); + dest.writeLong(minRoamingDownlinkBandwidth); + dest.writeLong(minRoamingUplinkBandwidth); + dest.writeStringArray(excludedSsidList); + writeProtoPortMap(dest, requiredProtoPortMap); + dest.writeInt(maximumBssLoadValue); + writeRoamingPartnerList(dest, flags, preferredRoamingPartnerList); + dest.writeParcelable(policyUpdate, flags); + } + + @Override + public boolean equals(Object thatObject) { + if (this == thatObject) { + return true; + } + if (!(thatObject instanceof Policy)) { + return false; + } + Policy that = (Policy) thatObject; + + return minHomeDownlinkBandwidth == that.minHomeDownlinkBandwidth + && minHomeUplinkBandwidth == that.minHomeUplinkBandwidth + && minRoamingDownlinkBandwidth == that.minRoamingDownlinkBandwidth + && minRoamingUplinkBandwidth == that.minRoamingUplinkBandwidth + && Arrays.equals(excludedSsidList, that.excludedSsidList) + && (requiredProtoPortMap == null) ? that.requiredProtoPortMap == null + : requiredProtoPortMap.equals(that.requiredProtoPortMap) + && maximumBssLoadValue == that.maximumBssLoadValue + && (preferredRoamingPartnerList == null) ? that.preferredRoamingPartnerList == null + : preferredRoamingPartnerList.equals(that.preferredRoamingPartnerList) + && (policyUpdate == null) ? that.policyUpdate == null + : policyUpdate.equals(that.policyUpdate); + } + + /** + * Validate Policy data. + * + * @return true on success + */ + public boolean validate() { + if (policyUpdate == null) { + Log.d(TAG, "PolicyUpdate not specified"); + return false; + } + if (!policyUpdate.validate()) { + return false; + } + + // Validate SSID exclusion list. + if (excludedSsidList != null) { + if (excludedSsidList.length > MAX_EXCLUSION_SSIDS) { + Log.d(TAG, "SSID exclusion list size exceeded the max: " + + excludedSsidList.length); + return false; + } + for (String ssid : excludedSsidList) { + if (ssid.getBytes(StandardCharsets.UTF_8).length > MAX_SSID_BYTES) { + Log.d(TAG, "Invalid SSID: " + ssid); + return false; + } + } + } + // Validate required protocol to port map. + if (requiredProtoPortMap != null) { + for (Map.Entry entry : requiredProtoPortMap.entrySet()) { + String portNumber = entry.getValue(); + if (portNumber.getBytes(StandardCharsets.UTF_8).length > MAX_PORT_STRING_BYTES) { + Log.d(TAG, "PortNumber string bytes exceeded the max: " + portNumber); + return false; + } + } + } + // Validate preferred roaming partner list. + if (preferredRoamingPartnerList != null) { + for (RoamingPartner partner : preferredRoamingPartnerList) { + if (!partner.validate()) { + return false; + } + } + } + return true; + } + + public static final Creator CREATOR = + new Creator() { + @Override + public Policy createFromParcel(Parcel in) { + Policy policy = new Policy(); + policy.minHomeDownlinkBandwidth = in.readLong(); + policy.minHomeUplinkBandwidth = in.readLong(); + policy.minRoamingDownlinkBandwidth = in.readLong(); + policy.minRoamingUplinkBandwidth = in.readLong(); + policy.excludedSsidList = in.createStringArray(); + policy.requiredProtoPortMap = readProtoPortMap(in); + policy.maximumBssLoadValue = in.readInt(); + policy.preferredRoamingPartnerList = readRoamingPartnerList(in); + policy.policyUpdate = in.readParcelable(null); + return policy; + } + + @Override + public Policy[] newArray(int size) { + return new Policy[size]; + } + + /** + * Helper function for reading IP Protocol to Port Number map from a Parcel. + * + * @param in The Parcel to read from + * @return Map of IP protocol to port number + */ + private Map readProtoPortMap(Parcel in) { + int size = in.readInt(); + if (size == NULL_VALUE) { + return null; + } + Map protoPortMap = new HashMap<>(size); + for (int i = 0; i < size; i++) { + int key = in.readInt(); + String value = in.readString(); + protoPortMap.put(key, value); + } + return protoPortMap; + } + + /** + * Helper function for reading roaming partner list from a Parcel. + * + * @param in The Parcel to read from + * @return List of roaming partners + */ + private List readRoamingPartnerList(Parcel in) { + int size = in.readInt(); + if (size == NULL_VALUE) { + return null; + } + List partnerList = new ArrayList<>(); + for (int i = 0; i < size; i++) { + partnerList.add(in.readParcelable(null)); + } + return partnerList; + } + + }; + + /** + * Helper function for writing IP Protocol to Port Number map to a Parcel. + * + * @param dest The Parcel to write to + * @param protoPortMap The map to write + */ + private static void writeProtoPortMap(Parcel dest, Map protoPortMap) { + if (protoPortMap == null) { + dest.writeInt(NULL_VALUE); + return; + } + dest.writeInt(protoPortMap.size()); + for (Map.Entry entry : protoPortMap.entrySet()) { + dest.writeInt(entry.getKey()); + dest.writeString(entry.getValue()); + } + } + + /** + * Helper function for writing roaming partner list to a Parcel. + * + * @param dest The Parcel to write to + * @param flags The flag about how the object should be written + * @param partnerList The partner list to write + */ + private static void writeRoamingPartnerList(Parcel dest, int flags, + List partnerList) { + if (partnerList == null) { + dest.writeInt(NULL_VALUE); + return; + } + dest.writeInt(partnerList.size()); + for (RoamingPartner partner : partnerList) { + dest.writeParcelable(partner, flags); + } + } +} diff --git a/wifi/java/android/net/wifi/hotspot2/pps/UpdateParameter.aidl b/wifi/java/android/net/wifi/hotspot2/pps/UpdateParameter.aidl new file mode 100644 index 0000000000000..701db479076a7 --- /dev/null +++ b/wifi/java/android/net/wifi/hotspot2/pps/UpdateParameter.aidl @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2017, 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 android.net.wifi.hotspot2.pps; + +parcelable UpdateParameter; diff --git a/wifi/java/android/net/wifi/hotspot2/pps/UpdateParameter.java b/wifi/java/android/net/wifi/hotspot2/pps/UpdateParameter.java new file mode 100644 index 0000000000000..a390df72f0614 --- /dev/null +++ b/wifi/java/android/net/wifi/hotspot2/pps/UpdateParameter.java @@ -0,0 +1,303 @@ +/** + * Copyright (c) 2017, 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 android.net.wifi.hotspot2.pps; + +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; +import android.util.Base64; +import android.util.Log; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +/** + * Class representing configuration parameters for subscription or policy update in + * PerProviderSubscription (PPS) Management Object (MO) tree. This is used by both + * PerProviderSubscription/Policy/PolicyUpdate and PerProviderSubscription/SubscriptionUpdate + * subtree. + * + * For more info, refer to Hotspot 2.0 PPS MO defined in section 9.1 of the Hotspot 2.0 + * Release 2 Technical Specification. + * + * @hide + */ +public final class UpdateParameter implements Parcelable { + private static final String TAG = "UpdateParameter"; + + /** + * Value indicating policy update is not applicable. Thus, never check with policy server + * for updates. + */ + public static final long UPDATE_CHECK_INTERVAL_NEVER = 0xFFFFFFFFL; + + /** + * Valid string for UpdateMethod. + */ + public static final String UPDATE_METHOD_OMADM = "OMA-DM-ClientInitiated"; + public static final String UPDATE_METHOD_SSP = "SSP-ClientInitiated"; + + /** + * Valid string for Restriction. + */ + public static final String UPDATE_RESTRICTION_HOMESP = "HomeSP"; + public static final String UPDATE_RESTRICTION_ROAMING_PARTNER = "RoamingPartner"; + public static final String UPDATE_RESTRICTION_UNRESTRICTED = "Unrestricted"; + + /** + * Maximum bytes for URI string. + */ + private static final int MAX_URI_BYTES = 1023; + + /** + * Maximum bytes for URI string. + */ + private static final int MAX_URL_BYTES = 1023; + + /** + * Maximum bytes for username. + */ + private static final int MAX_USERNAME_BYTES = 63; + + /** + * Maximum bytes for password. + */ + private static final int MAX_PASSWORD_BYTES = 255; + + /** + * Number of bytes for certificate SHA-256 fingerprint byte array. + */ + private static final int CERTIFICATE_SHA256_BYTES = 32; + + /** + * This specifies how often the mobile device shall check with policy server for updates. + * + * Using Long.MIN_VALUE to indicate unset value. + */ + public long updateIntervalInMinutes = Long.MIN_VALUE; + + /** + * The method used to update the policy. Permitted values are "OMA-DM-ClientInitiated" + * and "SPP-ClientInitiated". + */ + public String updateMethod = null; + + /** + * This specifies the hotspots at which the subscription update is permitted. Permitted + * values are "HomeSP", "RoamingPartner", or "Unrestricted"; + */ + public String restriction = null; + + /** + * The URI of the update server. + */ + public String serverUri = null; + + /** + * Username used to authenticate with the policy server. + */ + public String username = null; + + /** + * Base64 encoded password used to authenticate with the policy server. + */ + public String base64EncodedPassword = null; + + /** + * HTTPS URL for retrieving certificate for trust root. The trust root is used to validate + * policy server's identity. + */ + public String trustRootCertUrl = null; + + /** + * SHA-256 fingerprint of the certificate located at {@link #trustRootCertUrl} + */ + public byte[] trustRootCertSha256Fingerprint = null; + + /** + * Constructor for creating Policy with default values. + */ + public UpdateParameter() {} + + /** + * Copy constructor. + * + * @param source The source to copy from + */ + public UpdateParameter(UpdateParameter source) { + if (source == null) { + return; + } + updateIntervalInMinutes = source.updateIntervalInMinutes; + updateMethod = source.updateMethod; + restriction = source.restriction; + serverUri = source.serverUri; + username = source.username; + base64EncodedPassword = source.base64EncodedPassword; + trustRootCertUrl = source.trustRootCertUrl; + if (source.trustRootCertSha256Fingerprint != null) { + trustRootCertSha256Fingerprint = Arrays.copyOf(source.trustRootCertSha256Fingerprint, + source.trustRootCertSha256Fingerprint.length); + } + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeLong(updateIntervalInMinutes); + dest.writeString(updateMethod); + dest.writeString(restriction); + dest.writeString(serverUri); + dest.writeString(username); + dest.writeString(base64EncodedPassword); + dest.writeString(trustRootCertUrl); + dest.writeByteArray(trustRootCertSha256Fingerprint); + } + + @Override + public boolean equals(Object thatObject) { + if (this == thatObject) { + return true; + } + if (!(thatObject instanceof UpdateParameter)) { + return false; + } + UpdateParameter that = (UpdateParameter) thatObject; + + return updateIntervalInMinutes == that.updateIntervalInMinutes + && TextUtils.equals(updateMethod, that.updateMethod) + && TextUtils.equals(restriction, that.restriction) + && TextUtils.equals(serverUri, that.serverUri) + && TextUtils.equals(username, that.username) + && TextUtils.equals(base64EncodedPassword, that.base64EncodedPassword) + && TextUtils.equals(trustRootCertUrl, that.trustRootCertUrl) + && Arrays.equals(trustRootCertSha256Fingerprint, + that.trustRootCertSha256Fingerprint); + } + + /** + * Validate UpdateParameter data. + * + * @return true on success + */ + public boolean validate() { + if (updateIntervalInMinutes == Long.MIN_VALUE) { + Log.d(TAG, "Update interval not specified"); + return false; + } + // Update not applicable. + if (updateIntervalInMinutes == UPDATE_CHECK_INTERVAL_NEVER) { + return true; + } + + if (!TextUtils.equals(updateMethod, UPDATE_METHOD_OMADM) + && !TextUtils.equals(updateMethod, UPDATE_METHOD_SSP)) { + Log.d(TAG, "Unknown update method: " + updateMethod); + return false; + } + + if (!TextUtils.equals(restriction, UPDATE_RESTRICTION_HOMESP) + && !TextUtils.equals(restriction, UPDATE_RESTRICTION_ROAMING_PARTNER) + && !TextUtils.equals(restriction, UPDATE_RESTRICTION_UNRESTRICTED)) { + Log.d(TAG, "Unknown restriction: " + restriction); + return false; + } + + if (TextUtils.isEmpty(serverUri)) { + Log.d(TAG, "Missing update server URI"); + return false; + } + if (serverUri.getBytes(StandardCharsets.UTF_8).length > MAX_URI_BYTES) { + Log.d(TAG, "URI bytes exceeded the max: " + + serverUri.getBytes(StandardCharsets.UTF_8).length); + return false; + } + + if (TextUtils.isEmpty(username)) { + Log.d(TAG, "Missing username"); + return false; + } + if (username.getBytes(StandardCharsets.UTF_8).length > MAX_USERNAME_BYTES) { + Log.d(TAG, "Username bytes exceeded the max: " + + username.getBytes(StandardCharsets.UTF_8).length); + return false; + } + + if (TextUtils.isEmpty(base64EncodedPassword)) { + Log.d(TAG, "Missing username"); + return false; + } + if (base64EncodedPassword.getBytes(StandardCharsets.UTF_8).length > MAX_PASSWORD_BYTES) { + Log.d(TAG, "Password bytes exceeded the max: " + + base64EncodedPassword.getBytes(StandardCharsets.UTF_8).length); + return false; + } + try { + Base64.decode(base64EncodedPassword, Base64.DEFAULT); + } catch (IllegalArgumentException e) { + Log.d(TAG, "Invalid encoding for password: " + base64EncodedPassword); + return false; + } + + if (TextUtils.isEmpty(trustRootCertUrl)) { + Log.d(TAG, "Missing trust root certificate URL"); + return false; + } + if (trustRootCertUrl.getBytes(StandardCharsets.UTF_8).length > MAX_URL_BYTES) { + Log.d(TAG, "Trust root cert URL bytes exceeded the max: " + + trustRootCertUrl.getBytes(StandardCharsets.UTF_8).length); + return false; + } + + if (trustRootCertSha256Fingerprint == null) { + Log.d(TAG, "Missing trust root certificate SHA-256 fingerprint"); + return false; + } + if (trustRootCertSha256Fingerprint.length != CERTIFICATE_SHA256_BYTES) { + Log.d(TAG, "Incorrect size of trust root certificate SHA-256 fingerprint: " + + trustRootCertSha256Fingerprint.length); + return false; + } + return true; + } + + public static final Creator CREATOR = + new Creator() { + @Override + public UpdateParameter createFromParcel(Parcel in) { + UpdateParameter updateParam = new UpdateParameter(); + updateParam.updateIntervalInMinutes = in.readLong(); + updateParam.updateMethod = in.readString(); + updateParam.restriction = in.readString(); + updateParam.serverUri = in.readString(); + updateParam.username = in.readString(); + updateParam.base64EncodedPassword = in.readString(); + updateParam.trustRootCertUrl = in.readString(); + updateParam.trustRootCertSha256Fingerprint = in.createByteArray(); + return updateParam; + } + + @Override + public UpdateParameter[] newArray(int size) { + return new UpdateParameter[size]; + } + }; +} diff --git a/wifi/tests/assets/hsr1/HSR1ProfileWithCACert.base64 b/wifi/tests/assets/hsr1/HSR1ProfileWithCACert.base64 index 8c1eb08672989..995963d2b4ccc 100644 --- a/wifi/tests/assets/hsr1/HSR1ProfileWithCACert.base64 +++ b/wifi/tests/assets/hsr1/HSR1ProfileWithCACert.base64 @@ -42,7 +42,7 @@ V1UrQ2lBZ0lDQWdJQ0FnSUNBOFRtOWtaVDRLSUNBZ0lDQWdJQ0FnSUNBZ1BFNXZaR1ZPWVcxbApQ a05sY25ScFptbGpZWFJsVkhsd1pUd3ZUbTlrWlU1aGJXVStDaUFnSUNBZ0lDQWdJQ0FnSUR4V1lX eDFaVDU0TlRBNWRqTThMMVpoCmJIVmxQZ29nSUNBZ0lDQWdJQ0FnUEM5T2IyUmxQZ29nSUNBZ0lD QWdJQ0FnUEU1dlpHVStDaUFnSUNBZ0lDQWdJQ0FnSUR4T2IyUmwKVG1GdFpUNURaWEowVTBoQk1q -VTJSbWx1WjJWeVVISnBiblE4TDA1dlpHVk9ZVzFsUGdvZ0lDQWdJQ0FnSUNBZ0lDQThWbUZzZFdV +VTJSbWx1WjJWeWNISnBiblE4TDA1dlpHVk9ZVzFsUGdvZ0lDQWdJQ0FnSUNBZ0lDQThWbUZzZFdV KwpNV1l4WmpGbU1XWXhaakZtTVdZeFpqRm1NV1l4WmpGbU1XWXhaakZtTVdZeFpqRm1NV1l4WmpG bU1XWXhaakZtTVdZeFpqRm1NV1l4ClpqRm1NV1l4Wmp3dlZtRnNkV1UrQ2lBZ0lDQWdJQ0FnSUNB OEwwNXZaR1UrQ2lBZ0lDQWdJQ0FnUEM5T2IyUmxQZ29nSUNBZ0lDQWcKSUR4T2IyUmxQZ29nSUNB diff --git a/wifi/tests/assets/hsr1/HSR1ProfileWithCACert.conf b/wifi/tests/assets/hsr1/HSR1ProfileWithCACert.conf index 6d86dd53eb76a..3ddd09f91ff4b 100644 --- a/wifi/tests/assets/hsr1/HSR1ProfileWithCACert.conf +++ b/wifi/tests/assets/hsr1/HSR1ProfileWithCACert.conf @@ -35,7 +35,7 @@ ICAgPC9Ob2RlPgogICAgICAgIDxOb2RlPgogICAgICAgICAgPE5vZGVOYW1lPkRpZ2l0YWxDZXJ0 aWZpY2F0ZTwvTm9kZU5hbWU+CiAgICAgICAgICA8Tm9kZT4KICAgICAgICAgICAgPE5vZGVOYW1l PkNlcnRpZmljYXRlVHlwZTwvTm9kZU5hbWU+CiAgICAgICAgICAgIDxWYWx1ZT54NTA5djM8L1Zh bHVlPgogICAgICAgICAgPC9Ob2RlPgogICAgICAgICAgPE5vZGU+CiAgICAgICAgICAgIDxOb2Rl -TmFtZT5DZXJ0U0hBMjU2RmluZ2VyUHJpbnQ8L05vZGVOYW1lPgogICAgICAgICAgICA8VmFsdWU+ +TmFtZT5DZXJ0U0hBMjU2RmluZ2VycHJpbnQ8L05vZGVOYW1lPgogICAgICAgICAgICA8VmFsdWU+ MWYxZjFmMWYxZjFmMWYxZjFmMWYxZjFmMWYxZjFmMWYxZjFmMWYxZjFmMWYxZjFmMWYxZjFmMWYx ZjFmMWYxZjwvVmFsdWU+CiAgICAgICAgICA8L05vZGU+CiAgICAgICAgPC9Ob2RlPgogICAgICAg IDxOb2RlPgogICAgICAgICAgPE5vZGVOYW1lPlNJTTwvTm9kZU5hbWU+CiAgICAgICAgICA8Tm9k diff --git a/wifi/tests/assets/pps/PerProviderSubscription.xml b/wifi/tests/assets/pps/PerProviderSubscription.xml index 3969f697a3257..7f2d95de95e9f 100644 --- a/wifi/tests/assets/pps/PerProviderSubscription.xml +++ b/wifi/tests/assets/pps/PerProviderSubscription.xml @@ -7,6 +7,10 @@ urn:wfa:mo:hotspot2dot0-perprovidersubscription:1.0 + + UpdateIdentifier + 12 + i001 @@ -143,7 +147,7 @@ x509v3 - CertSHA256FingerPrint + CertSHA256Fingerprint 1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f @@ -159,6 +163,237 @@ + + Policy + + PreferredRoamingPartnerList + + p001 + + FQDN_Match + test1.fqdn.com,exactMatch + + + Priority + 127 + + + Country + us,fr + + + + p002 + + FQDN_Match + test2.fqdn.com,includeSubdomains + + + Priority + 200 + + + Country + * + + + + + MinBackhaulThreshold + + m001 + + NetworkType + home + + + DLBandwidth + 23412 + + + ULBandwidth + 9823 + + + + m002 + + NetworkType + roaming + + + DLBandwidth + 9271 + + + ULBandwidth + 2315 + + + + + PolicyUpdate + + UpdateInterval + 120 + + + UpdateMethod + OMA-DM-ClientInitiated + + + Restriction + HomeSP + + + URI + policy.update.com + + + UsernamePassword + + Username + updateUser + + + Password + updatePass + + + + TrustRoot + + CertURL + update.cert.com + + + CertSHA256Fingerprint + 1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f + + + + + SPExclusionList + + s001 + + SSID + excludeSSID + + + + + RequiredProtoPortTuple + + r001 + + IPProtocol + 12 + + + PortNumber + 34,92,234 + + + + + MaximumBSSLoadValue + 23 + + + + CredentialPriority + 99 + + + AAAServerTrustRoot + + a001 + + CertURL + server1.trust.root.com + + + CertSHA256Fingerprint + 1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f + + + + + SubscriptionUpdate + + UpdateInterval + 120 + + + UpdateMethod + SSP-ClientInitiated + + + Restriction + RoamingPartner + + + URI + subscription.update.com + + + UsernamePassword + + Username + subscriptionUser + + + Password + subscriptionPass + + + + TrustRoot + + CertURL + subscription.update.cert.com + + + CertSHA256Fingerprint + 1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f + + + + + SubscriptionParameter + + CreationDate + 2016-02-01T10:00:00Z + + + ExpirationDate + 2016-03-01T10:00:00Z + + + TypeOfSubscription + Gold + + + UsageLimits + + DataLimit + 921890 + + + StartDate + 2016-12-01T10:00:00Z + + + TimeLimit + 120 + + + UsageTimePeriod + 99910 + + + diff --git a/wifi/tests/src/android/net/wifi/hotspot2/PasspointConfigurationTest.java b/wifi/tests/src/android/net/wifi/hotspot2/PasspointConfigurationTest.java index 2350d3201171b..1eb08e0d3b582 100644 --- a/wifi/tests/src/android/net/wifi/hotspot2/PasspointConfigurationTest.java +++ b/wifi/tests/src/android/net/wifi/hotspot2/PasspointConfigurationTest.java @@ -22,16 +22,26 @@ import static org.junit.Assert.assertTrue; import android.net.wifi.EAPConstants; import android.net.wifi.hotspot2.pps.Credential; import android.net.wifi.hotspot2.pps.HomeSP; +import android.net.wifi.hotspot2.pps.Policy; +import android.net.wifi.hotspot2.pps.UpdateParameter; import android.os.Parcel; import android.test.suitebuilder.annotation.SmallTest; +import android.util.Base64; import org.junit.Test; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; + /** * Unit tests for {@link android.net.wifi.hotspot2.PasspointConfiguration}. */ @SmallTest public class PasspointConfigurationTest { + private static final int MAX_URL_BYTES = 1023; + private static final int CERTIFICATE_FINGERPRINT_BYTES = 32; /** * Utility function for creating a {@link android.net.wifi.hotspot2.pps.HomeSP}. @@ -65,6 +75,93 @@ public class PasspointConfigurationTest { return cred; } + /** + * Helper function for creating a {@link Policy} for testing. + * + * @return {@link Policy} + */ + private static Policy createPolicy() { + Policy policy = new Policy(); + policy.minHomeDownlinkBandwidth = 123; + policy.minHomeUplinkBandwidth = 345; + policy.minRoamingDownlinkBandwidth = 567; + policy.minRoamingUplinkBandwidth = 789; + policy.maximumBssLoadValue = 12; + policy.excludedSsidList = new String[] {"ssid1", "ssid2"}; + policy.requiredProtoPortMap = new HashMap<>(); + policy.requiredProtoPortMap.put(12, "23,342,123"); + policy.requiredProtoPortMap.put(23, "789,372,1235"); + + policy.preferredRoamingPartnerList = new ArrayList<>(); + Policy.RoamingPartner partner1 = new Policy.RoamingPartner(); + partner1.fqdn = "partner1.com"; + partner1.fqdnExactMatch = true; + partner1.priority = 12; + partner1.countries = "us,jp"; + Policy.RoamingPartner partner2 = new Policy.RoamingPartner(); + partner2.fqdn = "partner2.com"; + partner2.fqdnExactMatch = false; + partner2.priority = 42; + partner2.countries = "ca,fr"; + policy.preferredRoamingPartnerList.add(partner1); + policy.preferredRoamingPartnerList.add(partner2); + + policy.policyUpdate = new UpdateParameter(); + policy.policyUpdate.updateIntervalInMinutes = 1712; + policy.policyUpdate.updateMethod = UpdateParameter.UPDATE_METHOD_OMADM; + policy.policyUpdate.restriction = UpdateParameter.UPDATE_RESTRICTION_HOMESP; + policy.policyUpdate.serverUri = "policy.update.com"; + policy.policyUpdate.username = "username"; + policy.policyUpdate.base64EncodedPassword = + Base64.encodeToString("password".getBytes(), Base64.DEFAULT); + policy.policyUpdate.trustRootCertUrl = "trust.cert.com"; + policy.policyUpdate.trustRootCertSha256Fingerprint = + new byte[CERTIFICATE_FINGERPRINT_BYTES]; + + return policy; + } + + private static UpdateParameter createSubscriptionUpdate() { + UpdateParameter subUpdate = new UpdateParameter(); + subUpdate.updateIntervalInMinutes = 9021; + subUpdate.updateMethod = UpdateParameter.UPDATE_METHOD_SSP; + subUpdate.restriction = UpdateParameter.UPDATE_RESTRICTION_ROAMING_PARTNER; + subUpdate.serverUri = "subscription.update.com"; + subUpdate.username = "subUsername"; + subUpdate.base64EncodedPassword = + Base64.encodeToString("subPassword".getBytes(), Base64.DEFAULT); + subUpdate.trustRootCertUrl = "subscription.trust.cert.com"; + subUpdate.trustRootCertSha256Fingerprint = new byte[CERTIFICATE_FINGERPRINT_BYTES]; + return subUpdate; + } + /** + * Helper function for creating a {@link PasspointConfiguration} for testing. + * + * @return {@link PasspointConfiguration} + */ + private static PasspointConfiguration createConfig() { + PasspointConfiguration config = new PasspointConfiguration(); + config.homeSp = createHomeSp(); + config.credential = createCredential(); + config.policy = createPolicy(); + config.subscriptionUpdate = createSubscriptionUpdate(); + config.trustRootCertList = new HashMap<>(); + config.trustRootCertList.put("trustRoot.cert1.com", + new byte[CERTIFICATE_FINGERPRINT_BYTES]); + config.trustRootCertList.put("trustRoot.cert2.com", + new byte[CERTIFICATE_FINGERPRINT_BYTES]); + config.updateIdentifier = 1; + config.credentialPriority = 120; + config.subscriptionCreationTimeInMs = 231200; + config.subscriptionExpirationTimeInMs = 2134232; + config.subscriptionType = "Gold"; + config.usageLimitUsageTimePeriodInMinutes = 3600; + config.usageLimitStartTimeInMs = 124214213; + config.usageLimitDataLimit = 14121; + config.usageLimitTimeLimitInMinutes = 78912; + return config; + } + /** * Verify parcel write and read consistency for the given configuration. * @@ -92,39 +189,73 @@ public class PasspointConfigurationTest { } /** - * Verify parcel read/write for a configuration that contained both HomeSP and Credential. + * Verify parcel read/write for a configuration that contained the full configuration. * * @throws Exception */ @Test - public void verifyParcelWithHomeSPAndCredential() throws Exception { - PasspointConfiguration config = new PasspointConfiguration(); - config.homeSp = createHomeSp(); - config.credential = createCredential(); + public void verifyParcelWithFullConfiguration() throws Exception { + verifyParcel(createConfig()); + } + + /** + * Verify parcel read/write for a configuration that doesn't contain HomeSP. + * + * @throws Exception + */ + @Test + public void verifyParcelWithoutHomeSP() throws Exception { + PasspointConfiguration config = createConfig(); + config.homeSp = null; verifyParcel(config); } /** - * Verify parcel read/write for a configuration that contained only HomeSP. + * Verify parcel read/write for a configuration that doesn't contain Credential. * * @throws Exception */ @Test - public void verifyParcelWithHomeSPOnly() throws Exception { - PasspointConfiguration config = new PasspointConfiguration(); - config.homeSp = createHomeSp(); + public void verifyParcelWithoutCredential() throws Exception { + PasspointConfiguration config = createConfig(); + config.credential = null; verifyParcel(config); } /** - * Verify parcel read/write for a configuration that contained only Credential. + * Verify parcel read/write for a configuration that doesn't contain Policy. * * @throws Exception */ @Test - public void verifyParcelWithCredentialOnly() throws Exception { - PasspointConfiguration config = new PasspointConfiguration(); - config.credential = createCredential(); + public void verifyParcelWithoutPolicy() throws Exception { + PasspointConfiguration config = createConfig(); + config.policy = null; + verifyParcel(config); + } + + /** + * Verify parcel read/write for a configuration that doesn't contain subscription update. + * + * @throws Exception + */ + @Test + public void verifyParcelWithoutSubscriptionUpdate() throws Exception { + PasspointConfiguration config = createConfig(); + config.subscriptionUpdate = null; + verifyParcel(config); + } + + /** + * Verify parcel read/write for a configuration that doesn't contain trust root certificate + * list. + * + * @throws Exception + */ + @Test + public void verifyParcelWithoutTrustRootCertList() throws Exception { + PasspointConfiguration config = createConfig(); + config.trustRootCertList = null; verifyParcel(config); } @@ -139,6 +270,17 @@ public class PasspointConfigurationTest { assertFalse(config.validate()); } + /** + * Verify that a configuration contained all fields is valid. + * + * @throws Exception + */ + @Test + public void validateFullConfig() throws Exception { + PasspointConfiguration config = createConfig(); + assertTrue(config.validate()); + } + /** * Verify that a configuration without Credential is invalid. * @@ -146,36 +288,90 @@ public class PasspointConfigurationTest { */ @Test public void validateConfigWithoutCredential() throws Exception { - PasspointConfiguration config = new PasspointConfiguration(); - config.homeSp = createHomeSp(); + PasspointConfiguration config = createConfig(); + config.credential = null; assertFalse(config.validate()); } /** - * Verify that a a configuration without HomeSP is invalid. + * Verify that a configuration without HomeSP is invalid. * * @throws Exception */ @Test public void validateConfigWithoutHomeSp() throws Exception { - PasspointConfiguration config = new PasspointConfiguration(); - config.credential = createCredential(); + PasspointConfiguration config = createConfig(); + config.homeSp = null; assertFalse(config.validate()); } /** - * Verify a valid configuration. + * Verify that a configuration without Policy is valid, since Policy configurations + * are optional (applied for Hotspot 2.0 Release only). * * @throws Exception */ @Test - public void validateValidConfig() throws Exception { - PasspointConfiguration config = new PasspointConfiguration(); - config.homeSp = createHomeSp(); - config.credential = createCredential(); + public void validateConfigWithoutPolicy() throws Exception { + PasspointConfiguration config = createConfig(); + config.policy = null; assertTrue(config.validate()); } + /** + * Verify that a configuration without subscription update is valid, since subscription + * update configurations are optional (applied for Hotspot 2.0 Release only). + * + * @throws Exception + */ + @Test + public void validateConfigWithoutSubscriptionUpdate() throws Exception { + PasspointConfiguration config = createConfig(); + config.subscriptionUpdate = null; + assertTrue(config.validate()); + } + + /** + * Verify that a configuration with a trust root certificate URL exceeding the max size + * is invalid. + * + * @throws Exception + */ + @Test + public void validateConfigWithInvalidTrustRootCertUrl() throws Exception { + PasspointConfiguration config = createConfig(); + byte[] rawUrlBytes = new byte[MAX_URL_BYTES + 1]; + Arrays.fill(rawUrlBytes, (byte) 'a'); + config.trustRootCertList.put(new String(rawUrlBytes, StandardCharsets.UTF_8), + new byte[CERTIFICATE_FINGERPRINT_BYTES]); + assertFalse(config.validate()); + + config.trustRootCertList = new HashMap<>(); + config.trustRootCertList.put(null, new byte[CERTIFICATE_FINGERPRINT_BYTES]); + assertFalse(config.validate()); + } + + /** + * Verify that a configuration with an invalid trust root certificate fingerprint is invalid. + * + * @throws Exception + */ + @Test + public void validateConfigWithInvalidTrustRootCertFingerprint() throws Exception { + PasspointConfiguration config = createConfig(); + config.trustRootCertList = new HashMap<>(); + config.trustRootCertList.put("test.cert.com", new byte[CERTIFICATE_FINGERPRINT_BYTES + 1]); + assertFalse(config.validate()); + + config.trustRootCertList = new HashMap<>(); + config.trustRootCertList.put("test.cert.com", new byte[CERTIFICATE_FINGERPRINT_BYTES - 1]); + assertFalse(config.validate()); + + config.trustRootCertList = new HashMap<>(); + config.trustRootCertList.put("test.cert.com", null); + assertFalse(config.validate()); + } + /** * Verify that copy constructor works when pass in a null source. * @@ -195,9 +391,7 @@ public class PasspointConfigurationTest { */ @Test public void validateCopyConstructorWithValidSource() throws Exception { - PasspointConfiguration sourceConfig = new PasspointConfiguration(); - sourceConfig.homeSp = createHomeSp(); - sourceConfig.credential = createCredential(); + PasspointConfiguration sourceConfig = createConfig(); PasspointConfiguration copyConfig = new PasspointConfiguration(sourceConfig); assertTrue(copyConfig.equals(sourceConfig)); } diff --git a/wifi/tests/src/android/net/wifi/hotspot2/omadm/PPSMOParserTest.java b/wifi/tests/src/android/net/wifi/hotspot2/omadm/PPSMOParserTest.java index 1c7508e7d0f51..055204ce5b036 100644 --- a/wifi/tests/src/android/net/wifi/hotspot2/omadm/PPSMOParserTest.java +++ b/wifi/tests/src/android/net/wifi/hotspot2/omadm/PPSMOParserTest.java @@ -23,7 +23,10 @@ import android.net.wifi.hotspot2.omadm.PPSMOParser; import android.net.wifi.hotspot2.PasspointConfiguration; import android.net.wifi.hotspot2.pps.Credential; import android.net.wifi.hotspot2.pps.HomeSP; +import android.net.wifi.hotspot2.pps.Policy; +import android.net.wifi.hotspot2.pps.UpdateParameter; import android.test.suitebuilder.annotation.SmallTest; +import android.text.TextUtils; import org.junit.Test; @@ -33,6 +36,7 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.text.DateFormat; import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -81,7 +85,38 @@ public class PPSMOParserTest { * @return {@link PasspointConfiguration} */ private PasspointConfiguration generateConfigurationFromPPSMOTree() throws Exception { + DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + PasspointConfiguration config = new PasspointConfiguration(); + config.updateIdentifier = 12; + config.credentialPriority = 99; + + // AAA Server trust root. + config.trustRootCertList = new HashMap<>(); + byte[] certFingerprint = new byte[32]; + Arrays.fill(certFingerprint, (byte) 0x1f); + config.trustRootCertList.put("server1.trust.root.com", certFingerprint); + + // Subscription update. + config.subscriptionUpdate = new UpdateParameter(); + config.subscriptionUpdate.updateIntervalInMinutes = 120; + config.subscriptionUpdate.updateMethod = UpdateParameter.UPDATE_METHOD_SSP; + config.subscriptionUpdate.restriction = UpdateParameter.UPDATE_RESTRICTION_ROAMING_PARTNER; + config.subscriptionUpdate.serverUri = "subscription.update.com"; + config.subscriptionUpdate.username = "subscriptionUser"; + config.subscriptionUpdate.base64EncodedPassword = "subscriptionPass"; + config.subscriptionUpdate.trustRootCertUrl = "subscription.update.cert.com"; + config.subscriptionUpdate.trustRootCertSha256Fingerprint = new byte[32]; + Arrays.fill(config.subscriptionUpdate.trustRootCertSha256Fingerprint, (byte) 0x1f); + + // Subscription parameters. + config.subscriptionCreationTimeInMs = format.parse("2016-02-01T10:00:00Z").getTime(); + config.subscriptionExpirationTimeInMs = format.parse("2016-03-01T10:00:00Z").getTime(); + config.subscriptionType = "Gold"; + config.usageLimitDataLimit = 921890; + config.usageLimitStartTimeInMs = format.parse("2016-12-01T10:00:00Z").getTime(); + config.usageLimitTimeLimitInMinutes = 120; + config.usageLimitUsageTimePeriodInMinutes = 99910; // HomeSP configuration. config.homeSp = new HomeSP(); @@ -97,7 +132,6 @@ public class PPSMOParserTest { config.homeSp.otherHomePartners = new String[] {"other.fqdn.com"}; // Credential configuration. - DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); config.credential = new Credential(); config.credential.creationTimeInMs = format.parse("2016-01-01T10:00:00Z").getTime(); config.credential.expirationTimeInMs = format.parse("2016-02-01T10:00:00Z").getTime(); @@ -118,12 +152,46 @@ public class PPSMOParserTest { config.credential.simCredential = new Credential.SimCredential(); config.credential.simCredential.imsi = "imsi"; config.credential.simCredential.eapType = 24; + + // Policy configuration. + config.policy = new Policy(); + config.policy.preferredRoamingPartnerList = new ArrayList<>(); + Policy.RoamingPartner partner1 = new Policy.RoamingPartner(); + partner1.fqdn = "test1.fqdn.com"; + partner1.fqdnExactMatch = true; + partner1.priority = 127; + partner1.countries = "us,fr"; + Policy.RoamingPartner partner2 = new Policy.RoamingPartner(); + partner2.fqdn = "test2.fqdn.com"; + partner2.fqdnExactMatch = false; + partner2.priority = 200; + partner2.countries = "*"; + config.policy.preferredRoamingPartnerList.add(partner1); + config.policy.preferredRoamingPartnerList.add(partner2); + config.policy.minHomeDownlinkBandwidth = 23412; + config.policy.minHomeUplinkBandwidth = 9823; + config.policy.minRoamingDownlinkBandwidth = 9271; + config.policy.minRoamingUplinkBandwidth = 2315; + config.policy.excludedSsidList = new String[] {"excludeSSID"}; + config.policy.requiredProtoPortMap = new HashMap<>(); + config.policy.requiredProtoPortMap.put(12, "34,92,234"); + config.policy.maximumBssLoadValue = 23; + config.policy.policyUpdate = new UpdateParameter(); + config.policy.policyUpdate.updateIntervalInMinutes = 120; + config.policy.policyUpdate.updateMethod = UpdateParameter.UPDATE_METHOD_OMADM; + config.policy.policyUpdate.restriction = UpdateParameter.UPDATE_RESTRICTION_HOMESP; + config.policy.policyUpdate.serverUri = "policy.update.com"; + config.policy.policyUpdate.username = "updateUser"; + config.policy.policyUpdate.base64EncodedPassword = "updatePass"; + config.policy.policyUpdate.trustRootCertUrl = "update.cert.com"; + config.policy.policyUpdate.trustRootCertSha256Fingerprint = new byte[32]; + Arrays.fill(config.policy.policyUpdate.trustRootCertSha256Fingerprint, (byte) 0x1f); + return config; } /** - * Parse and verify all supported fields under PPS MO tree (currently only fields under - * HomeSP and Credential subtree). + * Parse and verify all supported fields under PPS MO tree. * * @throws Exception */ diff --git a/wifi/tests/src/android/net/wifi/hotspot2/pps/PolicyTest.java b/wifi/tests/src/android/net/wifi/hotspot2/pps/PolicyTest.java new file mode 100644 index 0000000000000..c371c497f26d8 --- /dev/null +++ b/wifi/tests/src/android/net/wifi/hotspot2/pps/PolicyTest.java @@ -0,0 +1,304 @@ +/* + * Copyright (C) 2017 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 android.net.wifi.hotspot2.pps; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.os.Parcel; +import android.test.suitebuilder.annotation.SmallTest; +import android.util.Base64; + +import org.junit.Test; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Unit tests for {@link android.net.wifi.hotspot2.pps.Policy}. + */ +@SmallTest +public class PolicyTest { + private static final int MAX_NUMBER_OF_EXCLUDED_SSIDS = 128; + private static final int MAX_SSID_BYTES = 32; + private static final int MAX_PORT_STRING_BYTES = 64; + + /** + * Helper function for creating a {@link Policy} for testing. + * + * @return {@link Policy} + */ + private static Policy createPolicy() { + Policy policy = new Policy(); + policy.minHomeDownlinkBandwidth = 123; + policy.minHomeUplinkBandwidth = 345; + policy.minRoamingDownlinkBandwidth = 567; + policy.minRoamingUplinkBandwidth = 789; + policy.excludedSsidList = new String[] {"ssid1", "ssid2"}; + policy.requiredProtoPortMap = new HashMap<>(); + policy.requiredProtoPortMap.put(12, "23,342,123"); + policy.requiredProtoPortMap.put(23, "789,372,1235"); + policy.maximumBssLoadValue = 12; + + policy.preferredRoamingPartnerList = new ArrayList<>(); + Policy.RoamingPartner partner1 = new Policy.RoamingPartner(); + partner1.fqdn = "partner1.com"; + partner1.fqdnExactMatch = true; + partner1.priority = 12; + partner1.countries = "us,jp"; + Policy.RoamingPartner partner2 = new Policy.RoamingPartner(); + partner2.fqdn = "partner2.com"; + partner2.fqdnExactMatch = false; + partner2.priority = 42; + partner2.countries = "ca,fr"; + policy.preferredRoamingPartnerList.add(partner1); + policy.preferredRoamingPartnerList.add(partner2); + + policy.policyUpdate = new UpdateParameter(); + policy.policyUpdate.updateIntervalInMinutes = 1712; + policy.policyUpdate.updateMethod = UpdateParameter.UPDATE_METHOD_OMADM; + policy.policyUpdate.restriction = UpdateParameter.UPDATE_RESTRICTION_HOMESP; + policy.policyUpdate.serverUri = "policy.update.com"; + policy.policyUpdate.username = "username"; + policy.policyUpdate.base64EncodedPassword = + Base64.encodeToString("password".getBytes(), Base64.DEFAULT); + policy.policyUpdate.trustRootCertUrl = "trust.cert.com"; + policy.policyUpdate.trustRootCertSha256Fingerprint = new byte[32]; + + return policy; + } + + /** + * Helper function for verifying Policy after parcel write then read. + * @param policyToWrite + * @throws Exception + */ + private static void verifyParcel(Policy policyToWrite) throws Exception { + Parcel parcel = Parcel.obtain(); + policyToWrite.writeToParcel(parcel, 0); + + parcel.setDataPosition(0); // Rewind data position back to the beginning for read. + Policy policyFromRead = Policy.CREATOR.createFromParcel(parcel); + assertTrue(policyFromRead.equals(policyToWrite)); + } + + /** + * Verify parcel read/write for an empty Policy. + * + * @throws Exception + */ + @Test + public void verifyParcelWithEmptyPolicy() throws Exception { + verifyParcel(new Policy()); + } + + /** + * Verify parcel read/write for a Policy with all fields set. + * + * @throws Exception + */ + @Test + public void verifyParcelWithFullPolicy() throws Exception { + verifyParcel(createPolicy()); + } + + /** + * Verify parcel read/write for a Policy without protocol port map. + * + * @throws Exception + */ + @Test + public void verifyParcelWithoutProtoPortMap() throws Exception { + Policy policy = createPolicy(); + policy.requiredProtoPortMap = null; + verifyParcel(policy); + } + + /** + * Verify parcel read/write for a Policy without preferred roaming partner list. + * + * @throws Exception + */ + @Test + public void verifyParcelWithoutPreferredRoamingPartnerList() throws Exception { + Policy policy = createPolicy(); + policy.preferredRoamingPartnerList = null; + verifyParcel(policy); + } + + /** + * Verify parcel read/write for a Policy without policy update parameters. + * + * @throws Exception + */ + @Test + public void verifyParcelWithoutPolicyUpdate() throws Exception { + Policy policy = createPolicy(); + policy.policyUpdate = null; + verifyParcel(policy); + } + + /** + * Verify that policy created using copy constructor with null source should be the same + * as the policy created using default constructor. + * + * @throws Exception + */ + @Test + public void verifyCopyConstructionWithNullSource() throws Exception { + Policy copyPolicy = new Policy(null); + Policy defaultPolicy = new Policy(); + assertTrue(defaultPolicy.equals(copyPolicy)); + } + + /** + * Verify that policy created using copy constructor with a valid source should be the + * same as the source. + * + * @throws Exception + */ + @Test + public void verifyCopyConstructionWithFullPolicy() throws Exception { + Policy policy = createPolicy(); + Policy copyPolicy = new Policy(policy); + assertTrue(policy.equals(copyPolicy)); + } + + /** + * Verify that a default policy (with no informatio) is invalid. + * + * @throws Exception + */ + @Test + public void validatePolicyWithDefault() throws Exception { + Policy policy = new Policy(); + assertFalse(policy.validate()); + } + + /** + * Verify that a policy created using {@link #createPolicy} is valid, since all fields are + * filled in with valid values. + * + * @throws Exception + */ + @Test + public void validatePolicyWithFullPolicy() throws Exception { + assertTrue(createPolicy().validate()); + } + + /** + * Verify that a policy without policy update parameters is invalid. + * + * @throws Exception + */ + @Test + public void validatePolicyWithoutPolicyUpdate() throws Exception { + Policy policy = createPolicy(); + policy.policyUpdate = null; + assertFalse(policy.validate()); + } + + /** + * Verify that a policy with invalid policy update parameters is invalid. + * + * @throws Exception + */ + @Test + public void validatePolicyWithInvalidPolicyUpdate() throws Exception { + Policy policy = createPolicy(); + policy.policyUpdate = new UpdateParameter(); + assertFalse(policy.validate()); + } + + /** + * Verify that a policy with a preferred roaming partner with FQDN not specified is invalid. + * + * @throws Exception + */ + @Test + public void validatePolicyWithRoamingPartnerWithoutFQDN() throws Exception { + Policy policy = createPolicy(); + Policy.RoamingPartner partner = new Policy.RoamingPartner(); + partner.fqdnExactMatch = true; + partner.priority = 12; + partner.countries = "us,jp"; + policy.preferredRoamingPartnerList.add(partner); + assertFalse(policy.validate()); + } + + /** + * Verify that a policy with a preferred roaming partner with countries not specified is + * invalid. + * + * @throws Exception + */ + @Test + public void validatePolicyWithRoamingPartnerWithoutCountries() throws Exception { + Policy policy = createPolicy(); + Policy.RoamingPartner partner = new Policy.RoamingPartner(); + partner.fqdn = "test.com"; + partner.fqdnExactMatch = true; + partner.priority = 12; + policy.preferredRoamingPartnerList.add(partner); + assertFalse(policy.validate()); + } + + /** + * Verify that a policy with a proto-port tuple that contains an invalid port string is + * invalid. + * + * @throws Exception + */ + @Test + public void validatePolicyWithInvalidPortStringInProtoPortMap() throws Exception { + Policy policy = createPolicy(); + byte[] rawPortBytes = new byte[MAX_PORT_STRING_BYTES + 1]; + policy.requiredProtoPortMap.put(324, new String(rawPortBytes, StandardCharsets.UTF_8)); + assertFalse(policy.validate()); + } + + /** + * Verify that a policy with number of excluded SSIDs exceeded the max is invalid. + * + * @throws Exception + */ + @Test + public void validatePolicyWithSsidExclusionListSizeExceededMax() throws Exception { + Policy policy = createPolicy(); + policy.excludedSsidList = new String[MAX_NUMBER_OF_EXCLUDED_SSIDS + 1]; + Arrays.fill(policy.excludedSsidList, "ssid"); + assertFalse(policy.validate()); + } + + /** + * Verify that a policy with an invalid SSID in the excluded SSID list is invalid. + * + * @throws Exception + */ + @Test + public void validatePolicyWithInvalidSsid() throws Exception { + Policy policy = createPolicy(); + byte[] rawSsidBytes = new byte[MAX_SSID_BYTES + 1]; + Arrays.fill(rawSsidBytes, (byte) 'a'); + policy.excludedSsidList = new String[] {new String(rawSsidBytes, StandardCharsets.UTF_8)}; + assertFalse(policy.validate()); + } +} diff --git a/wifi/tests/src/android/net/wifi/hotspot2/pps/UpdateParameterTest.java b/wifi/tests/src/android/net/wifi/hotspot2/pps/UpdateParameterTest.java new file mode 100644 index 0000000000000..6bf0db1b43586 --- /dev/null +++ b/wifi/tests/src/android/net/wifi/hotspot2/pps/UpdateParameterTest.java @@ -0,0 +1,348 @@ +/* + * Copyright (C) 2017 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 android.net.wifi.hotspot2.pps; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.os.Parcel; +import android.test.suitebuilder.annotation.SmallTest; +import android.util.Base64; + +import org.junit.Test; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Unit tests for {@link android.net.wifi.hotspot2.pps.UpdateParameter}. + */ +@SmallTest +public class UpdateParameterTest { + private static final int MAX_URI_BYTES = 1023; + private static final int MAX_URL_BYTES = 1023; + private static final int MAX_USERNAME_BYTES = 63; + private static final int MAX_PASSWORD_BYTES = 255; + private static final int CERTIFICATE_SHA256_BYTES = 32; + + /** + * Helper function for creating a {@link UpdateParameter} for testing. + * + * @return {@link UpdateParameter} + */ + private static UpdateParameter createUpdateParameter() { + UpdateParameter updateParam = new UpdateParameter(); + updateParam.updateIntervalInMinutes = 1712; + updateParam.updateMethod = UpdateParameter.UPDATE_METHOD_OMADM; + updateParam.restriction = UpdateParameter.UPDATE_RESTRICTION_HOMESP; + updateParam.serverUri = "server.pdate.com"; + updateParam.username = "username"; + updateParam.base64EncodedPassword = + Base64.encodeToString("password".getBytes(), Base64.DEFAULT); + updateParam.trustRootCertUrl = "trust.cert.com"; + updateParam.trustRootCertSha256Fingerprint = new byte[32]; + return updateParam; + } + + /** + * Helper function for verifying UpdateParameter after parcel write then read. + * @param paramToWrite The UpdateParamter to verify + * @throws Exception + */ + private static void verifyParcel(UpdateParameter paramToWrite) throws Exception { + Parcel parcel = Parcel.obtain(); + paramToWrite.writeToParcel(parcel, 0); + + parcel.setDataPosition(0); // Rewind data position back to the beginning for read. + UpdateParameter paramFromRead = UpdateParameter.CREATOR.createFromParcel(parcel); + assertTrue(paramFromRead.equals(paramToWrite)); + } + + /** + * Verify parcel read/write for an empty UpdateParameter. + * + * @throws Exception + */ + @Test + public void verifyParcelWithEmptyUpdateParameter() throws Exception { + verifyParcel(new UpdateParameter()); + } + + /** + * Verify parcel read/write for a UpdateParameter with all fields set. + * + * @throws Exception + */ + @Test + public void verifyParcelWithFullUpdateParameter() throws Exception { + verifyParcel(createUpdateParameter()); + } + + /** + * Verify that UpdateParameter created using copy constructor with null source should be the + * same as the UpdateParameter created using default constructor. + * + * @throws Exception + */ + @Test + public void verifyCopyConstructionWithNullSource() throws Exception { + UpdateParameter copyParam = new UpdateParameter(null); + UpdateParameter defaultParam = new UpdateParameter(); + assertTrue(defaultParam.equals(copyParam)); + } + + /** + * Verify that UpdateParameter created using copy constructor with a valid source should be the + * same as the source. + * + * @throws Exception + */ + @Test + public void verifyCopyConstructionWithFullUpdateParameter() throws Exception { + UpdateParameter origParam = createUpdateParameter(); + UpdateParameter copyParam = new UpdateParameter(origParam); + assertTrue(origParam.equals(copyParam)); + } + + /** + * Verify that a default UpdateParameter is invalid. + * + * @throws Exception + */ + @Test + public void validateUpdateParameterWithDefault() throws Exception { + UpdateParameter updateParam = new UpdateParameter(); + assertFalse(updateParam.validate()); + } + + /** + * Verify that an UpdateParameter created using {@link #createUpdateParameter} is valid, + * since all fields are filled in with valid values. + * + * @throws Exception + */ + @Test + public void validateUpdateParameterWithFullPolicy() throws Exception { + assertTrue(createUpdateParameter().validate()); + } + + /** + * Verify that an UpdateParameter with an unknown update method is invalid. + * + * @throws Exception + */ + @Test + public void validateUpdateParameterWithUnknowMethod() throws Exception { + UpdateParameter updateParam = createUpdateParameter(); + updateParam.updateMethod = "adsfasd"; + assertFalse(updateParam.validate()); + } + + /** + * Verify that an UpdateParameter with an unknown restriction is invalid. + * + * @throws Exception + */ + @Test + public void validateUpdateParameterWithUnknowRestriction() throws Exception { + UpdateParameter updateParam = createUpdateParameter(); + updateParam.restriction = "adsfasd"; + assertFalse(updateParam.validate()); + } + + /** + * Verify that an UpdateParameter with an username exceeding maximum size is invalid. + * + * @throws Exception + */ + @Test + public void validateUpdateParameterWithUsernameExceedingMaxSize() throws Exception { + UpdateParameter updateParam = createUpdateParameter(); + byte[] rawUsernameBytes = new byte[MAX_USERNAME_BYTES + 1]; + Arrays.fill(rawUsernameBytes, (byte) 'a'); + updateParam.username = new String(rawUsernameBytes, StandardCharsets.UTF_8); + assertFalse(updateParam.validate()); + } + + /** + * Verify that an UpdateParameter with an empty username is invalid. + * + * @throws Exception + */ + @Test + public void validateUpdateParameterWithEmptyUsername() throws Exception { + UpdateParameter updateParam = createUpdateParameter(); + updateParam.username = null; + assertFalse(updateParam.validate()); + } + + /** + * Verify that an UpdateParameter with a password exceeding maximum size is invalid. + * + * @throws Exception + */ + @Test + public void validateUpdateParameterWithPasswordExceedingMaxSize() throws Exception { + UpdateParameter updateParam = createUpdateParameter(); + byte[] rawPasswordBytes = new byte[MAX_PASSWORD_BYTES + 1]; + Arrays.fill(rawPasswordBytes, (byte) 'a'); + updateParam.base64EncodedPassword = new String(rawPasswordBytes, StandardCharsets.UTF_8); + assertFalse(updateParam.validate()); + } + + /** + * Verify that an UpdateParameter with an empty password is invalid. + * + * @throws Exception + */ + @Test + public void validateUpdateParameterWithEmptyPassword() throws Exception { + UpdateParameter updateParam = createUpdateParameter(); + updateParam.base64EncodedPassword = null; + assertFalse(updateParam.validate()); + } + + /** + * Verify that an UpdateParameter with a Base64 encoded password that contained invalid padding + * is invalid. + * + * @throws Exception + */ + @Test + public void validateUpdateParameterWithPasswordContainedInvalidPadding() throws Exception { + UpdateParameter updateParam = createUpdateParameter(); + updateParam.base64EncodedPassword = updateParam.base64EncodedPassword + "="; + assertFalse(updateParam.validate()); + } + + /** + * Verify that an UpdateParameter without trust root certificate URL is invalid. + * + * @throws Exception + */ + @Test + public void validateUpdateParameterWithoutTrustRootCertUrl() throws Exception { + UpdateParameter updateParam = createUpdateParameter(); + updateParam.trustRootCertUrl = null; + assertFalse(updateParam.validate()); + } + + /** + * Verify that an UpdateParameter with invalid trust root certificate URL is invalid. + * + * @throws Exception + */ + @Test + public void validateUpdateParameterWithInvalidTrustRootCertUrl() throws Exception { + UpdateParameter updateParam = createUpdateParameter(); + byte[] rawUrlBytes = new byte[MAX_URL_BYTES + 1]; + Arrays.fill(rawUrlBytes, (byte) 'a'); + updateParam.trustRootCertUrl = new String(rawUrlBytes, StandardCharsets.UTF_8); + assertFalse(updateParam.validate()); + } + + /** + * Verify that an UpdateParameter without trust root certificate SHA-256 fingerprint is + * invalid. + * + * @throws Exception + */ + @Test + public void validateUpdateParameterWithouttrustRootCertSha256Fingerprint() throws Exception { + UpdateParameter updateParam = createUpdateParameter(); + updateParam.trustRootCertSha256Fingerprint = null; + assertFalse(updateParam.validate()); + } + + /** + * Verify that an UpdateParameter with an incorrect size trust root certificate SHA-256 + * fingerprint is invalid. + * + * @throws Exception + */ + @Test + public void validateUpdateParameterWithInvalidtrustRootCertSha256Fingerprint() throws Exception { + UpdateParameter updateParam = createUpdateParameter(); + updateParam.trustRootCertSha256Fingerprint = new byte[CERTIFICATE_SHA256_BYTES + 1]; + assertFalse(updateParam.validate()); + + updateParam.trustRootCertSha256Fingerprint = new byte[CERTIFICATE_SHA256_BYTES - 1]; + assertFalse(updateParam.validate()); + } + + /** + * Verify that an UpdateParameter without server URI is invalid. + * + * @throws Exception + */ + @Test + public void validateUpdateParameterWithoutServerUri() throws Exception { + UpdateParameter updateParam = createUpdateParameter(); + updateParam.serverUri = null; + assertFalse(updateParam.validate()); + } + + /** + * Verify that an UpdateParameter with an invalid server URI is invalid. + * + * @throws Exception + */ + @Test + public void validatePolicyWithInvalidServerUri() throws Exception { + UpdateParameter updateParam = createUpdateParameter(); + byte[] rawUriBytes = new byte[MAX_URI_BYTES + 1]; + Arrays.fill(rawUriBytes, (byte) 'a'); + updateParam.serverUri = new String(rawUriBytes, StandardCharsets.UTF_8); + assertFalse(updateParam.validate()); + } + + /** + * Verify that an UpdateParameter with update interval set to "never" will not perform + * validation on other parameters, since update is not applicable in this case. + * + * @throws Exception + */ + @Test + public void validateUpdateParameterWithNoServerCheck() throws Exception { + UpdateParameter updateParam = new UpdateParameter(); + updateParam.updateIntervalInMinutes = UpdateParameter.UPDATE_CHECK_INTERVAL_NEVER; + updateParam.username = null; + updateParam.base64EncodedPassword = null; + updateParam.updateMethod = null; + updateParam.restriction = null; + updateParam.serverUri = null; + updateParam.trustRootCertUrl = null; + updateParam.trustRootCertSha256Fingerprint = null; + assertTrue(updateParam.validate()); + } + + /** + * Verify that an UpdateParameter with unset update interval is invalid. + * + * @throws Exception + */ + @Test + public void validateUpdateParameterWithoutUpdateInterval() throws Exception { + UpdateParameter updateParam = createUpdateParameter(); + updateParam.updateIntervalInMinutes = Long.MIN_VALUE; + assertFalse(updateParam.validate()); + } +}