Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ebb722d0 authored by Treehugger Robot's avatar Treehugger Robot Committed by Gerrit Code Review
Browse files

Merge "wifi: hotspot2: add support for validating passpoint configuration"

parents c02a4a46 a25d717d
Loading
Loading
Loading
Loading
+15 −0
Original line number Diff line number Diff line
@@ -61,6 +61,21 @@ public final class PasspointConfiguration implements Parcelable {
                    credential.equals(that.credential));
    }

    /**
     * Validate the configuration data.
     *
     * @return true on success or false on failure
     */
    public boolean validate() {
        if (homeSp == null || !homeSp.validate()) {
            return false;
        }
        if (credential == null || !credential.validate()) {
            return false;
        }
        return true;
    }

    public static final Creator<PasspointConfiguration> CREATOR =
        new Creator<PasspointConfiguration>() {
            @Override
+315 −3
Original line number Diff line number Diff line
@@ -16,15 +16,21 @@

package android.net.wifi.hotspot2.pps;

import android.net.wifi.EAPConstants;
import android.net.wifi.ParcelUtil;
import android.os.Parcelable;
import android.os.Parcel;
import android.text.TextUtils;
import android.util.Log;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

/**
 * Class representing Credential subtree in the PerProviderSubscription (PPS)
@@ -40,6 +46,14 @@ import java.util.Arrays;
 * @hide
 */
public final class Credential implements Parcelable {
    private static final String TAG = "Credential";

    /**
     * Max string length for realm.  Refer to Credential/Realm node in Hotspot 2.0 Release 2
     * Technical Specification Section 9.1 for more info.
     */
    private static final int MAX_REALM_LENGTH = 253;

    /**
     * The realm associated with this credential.  It will be used to determine
     * if this credential can be used to authenticate with a given hotspot by
@@ -52,6 +66,26 @@ public final class Credential implements Parcelable {
     * Contains the fields under PerProviderSubscription/Credential/UsernamePassword subtree.
     */
    public static final class UserCredential implements Parcelable {
        /**
         * Maximum string length for username.  Refer to Credential/UsernamePassword/Username
         * node in Hotspot 2.0 Release 2 Technical Specification Section 9.1 for more info.
         */
        private static final int MAX_USERNAME_LENGTH = 63;

        /**
         * Maximum string length for password.  Refer to Credential/UsernamePassword/Password
         * in Hotspot 2.0 Release 2 Technical Specification Section 9.1 for more info.
         */
        private static final int MAX_PASSWORD_LENGTH = 255;

        /**
         * Supported Non-EAP inner methods.  Refer to
         * Credential/UsernamePassword/EAPMethod/InnerEAPType in Hotspot 2.0 Release 2 Technical
         * Specification Section 9.1 for more info.
         */
        private static final Set<String> SUPPORTED_AUTH =
                new HashSet<String>(Arrays.asList("PAP", "CHAP", "MS-CHAP", "MS-CHAP-V2"));

        /**
         * Username of the credential.
         */
@@ -104,6 +138,44 @@ public final class Credential implements Parcelable {
                    TextUtils.equals(nonEapInnerMethod, that.nonEapInnerMethod);
        }

        /**
         * Validate the configuration data.
         *
         * @return true on success or false on failure
         */
        public boolean validate() {
            if (TextUtils.isEmpty(username)) {
                Log.d(TAG, "Missing username");
                return false;
            }
            if (username.length() > MAX_USERNAME_LENGTH) {
                Log.d(TAG, "username exceeding maximum length: " + username.length());
                return false;
            }

            if (TextUtils.isEmpty(password)) {
                Log.d(TAG, "Missing password");
                return false;
            }
            if (password.length() > MAX_PASSWORD_LENGTH) {
                Log.d(TAG, "password exceeding maximum length: " + password.length());
                return false;
            }

            // Only supports EAP-TTLS for user credential.
            if (eapType != EAPConstants.EAP_TTLS) {
                Log.d(TAG, "Invalid EAP Type for user credential: " + eapType);
                return false;
            }

            // Verify Non-EAP inner method for EAP-TTLS.
            if (!SUPPORTED_AUTH.contains(nonEapInnerMethod)) {
                Log.d(TAG, "Invalid non-EAP inner method for EAP-TTLS: " + nonEapInnerMethod);
                return false;
            }
            return true;
        }

        public static final Creator<UserCredential> CREATOR =
            new Creator<UserCredential>() {
                @Override
@@ -125,12 +197,22 @@ public final class Credential implements Parcelable {
    public UserCredential userCredential = null;

    /**
     * Certificate based credential.
     * Certificate based credential.  This is used for EAP-TLS.
     * Contains fields under PerProviderSubscription/Credential/DigitalCertificate subtree.
     */
    public static final class CertificateCredential implements Parcelable {
        /**
         * Certificate type. Valid values are "802.1ar" and "x509v3".
         * Supported certificate types.
         */
        private static final String CERT_TYPE_X509V3 = "x509v3";

        /**
         * Certificate SHA-256 fingerprint length.
         */
        private static final int CERT_SHA256_FINGER_PRINT_LENGTH = 32;

        /**
         * Certificate type.
         */
        public String certType = null;

@@ -164,6 +246,24 @@ public final class Credential implements Parcelable {
                    Arrays.equals(certSha256FingerPrint, that.certSha256FingerPrint);
        }

        /**
         * Validate the configuration data.
         *
         * @return true on success or false on failure
         */
        public boolean validate() {
            if (!TextUtils.equals(CERT_TYPE_X509V3, certType)) {
                Log.d(TAG, "Unsupported certificate type: " + certType);
                return false;
            }
            if (certSha256FingerPrint == null ||
                    certSha256FingerPrint.length != CERT_SHA256_FINGER_PRINT_LENGTH) {
                Log.d(TAG, "Invalid SHA-256 fingerprint");
                return false;
            }
            return true;
        }

        public static final Creator<CertificateCredential> CREATOR =
            new Creator<CertificateCredential>() {
                @Override
@@ -188,7 +288,14 @@ public final class Credential implements Parcelable {
     */
    public static final class SimCredential implements Parcelable {
        /**
         * International Mobile device Subscriber Identity.
         * Maximum string length for IMSI.
         */
        public static final int MAX_IMSI_LENGTH = 15;

        /**
         * International Mobile Subscriber Identity, is used to identify the user
         * of a cellular network and is a unique identification associated with all
         * cellular networks
         */
        public String imsi = null;

@@ -225,6 +332,26 @@ public final class Credential implements Parcelable {
            dest.writeInt(eapType);
        }

        /**
         * Validate the configuration data.
         *
         * @return true on success or false on failure
         */
        public boolean validate() {
            // Note: this only validate the format of IMSI string itself.  Additional verification
            // will be done by WifiService at the time of provisioning to verify against the IMSI
            // of the SIM card installed in the device.
            if (!verifyImsi()) {
                return false;
            }
            if (eapType != EAPConstants.EAP_SIM && eapType != EAPConstants.EAP_AKA &&
                    eapType != EAPConstants.EAP_AKA_PRIME) {
                Log.d(TAG, "Invalid EAP Type for SIM credential: " + eapType);
                return false;
            }
            return true;
        }

        public static final Creator<SimCredential> CREATOR =
            new Creator<SimCredential>() {
                @Override
@@ -240,6 +367,43 @@ public final class Credential implements Parcelable {
                    return new SimCredential[size];
                }
            };

        /**
         * Verify the IMSI (International Mobile Subscriber Identity) string.  The string
         * should contain zero or more numeric digits, and might ends with a "*" for prefix
         * matching.
         *
         * @return true if IMSI is valid, false otherwise.
         */
        private boolean verifyImsi() {
            if (TextUtils.isEmpty(imsi)) {
                Log.d(TAG, "Missing IMSI");
                return false;
            }
            if (imsi.length() > MAX_IMSI_LENGTH) {
                Log.d(TAG, "IMSI exceeding maximum length: " + imsi.length());
                return false;
            }

            // Locate the first non-digit character.
            int nonDigit;
            char stopChar = '\0';
            for (nonDigit = 0; nonDigit < imsi.length(); nonDigit++) {
                stopChar = imsi.charAt(nonDigit);
                if (stopChar < '0' || stopChar > '9') {
                    break;
                }
            }

            if (nonDigit == imsi.length()) {
                return true;
            }
            else if (nonDigit == imsi.length()-1 && stopChar == '*') {
                // Prefix matching.
                return true;
            }
            return false;
        }
    }
    public SimCredential simCredential = null;

@@ -296,6 +460,42 @@ public final class Credential implements Parcelable {
                isPrivateKeyEquals(clientPrivateKey, that.clientPrivateKey);
    }

    /**
     * Validate the configuration data.
     *
     * @return true on success or false on failure
     */
    public boolean validate() {
        if (TextUtils.isEmpty(realm)) {
            Log.d(TAG, "Missing realm");
            return false;
        }
        if (realm.length() > MAX_REALM_LENGTH) {
            Log.d(TAG, "realm exceeding maximum length: " + realm.length());
            return false;
        }

        // Verify the credential.
        if (userCredential != null) {
            if (!verifyUserCredential()) {
                return false;
            }
        } else if (certCredential != null) {
            if (!verifyCertCredential()) {
                return false;
            }
        } else if (simCredential != null) {
            if (!verifySimCredential()) {
                return false;
            }
        } else {
            Log.d(TAG, "Missing required credential");
            return false;
        }

        return true;
    }

    public static final Creator<Credential> CREATOR =
        new Creator<Credential>() {
            @Override
@@ -317,6 +517,91 @@ public final class Credential implements Parcelable {
            }
        };

    /**
     * Verify user credential.
     *
     * @return true if user credential is valid, false otherwise.
     */
    private boolean verifyUserCredential() {
        if (userCredential == null) {
            Log.d(TAG, "Missing user credential");
            return false;
        }
        if (certCredential != null || simCredential != null) {
            Log.d(TAG, "Contained more than one type of credential");
            return false;
        }
        if (!userCredential.validate()) {
            return false;
        }
        if (caCertificate == null) {
            Log.d(TAG, "Missing CA Certificate for user credential");
            return false;
        }
        return true;
    }

    /**
     * Verify certificate credential, which is used for EAP-TLS.  This will verify
     * that the necessary client key and certificates are provided.
     *
     * @return true if certificate credential is valid, false otherwise.
     */
    private boolean verifyCertCredential() {
        if (certCredential == null) {
            Log.d(TAG, "Missing certificate credential");
            return false;
        }
        if (userCredential != null || simCredential != null) {
            Log.d(TAG, "Contained more than one type of credential");
            return false;
        }

        if (!certCredential.validate()) {
            return false;
        }

        // Verify required key and certificates for certificate credential.
        if (caCertificate == null) {
            Log.d(TAG, "Missing CA Certificate for certificate credential");
            return false;
        }
        if (clientPrivateKey == null) {
            Log.d(TAG, "Missing client private key for certificate credential");
            return false;
        }
        try {
            // Verify SHA-256 fingerprint for client certificate.
            if (!verifySha256Fingerprint(clientCertificateChain,
                    certCredential.certSha256FingerPrint)) {
                Log.d(TAG, "SHA-256 fingerprint mismatch");
                return false;
            }
        } catch (NoSuchAlgorithmException | CertificateEncodingException e) {
            Log.d(TAG, "Failed to verify SHA-256 fingerprint: " + e.getMessage());
            return false;
        }

        return true;
    }

    /**
     * Verify SIM credential.
     *
     * @return true if SIM credential is valid, false otherwise.
     */
    private boolean verifySimCredential() {
        if (simCredential == null) {
            Log.d(TAG, "Missing SIM credential");
            return false;
        }
        if (userCredential != null || certCredential != null) {
            Log.d(TAG, "Contained more than one type of credential");
            return false;
        }
        return simCredential.validate();
    }

    private static boolean isPrivateKeyEquals(PrivateKey key1, PrivateKey key2) {
        if (key1 == null && key2 == null) {
            return true;
@@ -373,4 +658,31 @@ public final class Credential implements Parcelable {

        return true;
    }

    /**
     * Verify that the digest for a certificate in the certificate chain matches expected
     * fingerprint.  The certificate that matches the fingerprint is the client certificate.
     *
     * @param certChain Chain of certificates
     * @param expectedFingerprint The expected SHA-256 digest of the client certificate
     * @return true if the certificate chain contains a matching certificate, false otherwise
     * @throws NoSuchAlgorithmException
     * @throws CertificateEncodingException
     */
    private static boolean verifySha256Fingerprint(X509Certificate[] certChain,
                                                   byte[] expectedFingerprint)
            throws NoSuchAlgorithmException, CertificateEncodingException {
        if (certChain == null) {
            return false;
        }
        MessageDigest digester = MessageDigest.getInstance("SHA-256");
        for (X509Certificate certificate : certChain) {
            digester.reset();
            byte[] fingerprint = digester.digest(certificate.getEncoded());
            if (Arrays.equals(expectedFingerprint, fingerprint)) {
                return true;
            }
        }
        return false;
    }
}
+20 −0
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@ package android.net.wifi.hotspot2.pps;
import android.os.Parcelable;
import android.os.Parcel;
import android.text.TextUtils;
import android.util.Log;

import java.util.Arrays;

@@ -34,6 +35,8 @@ import java.util.Arrays;
 * @hide
 */
public final class HomeSP implements Parcelable {
    private static final String TAG = "HomeSP";

    /**
     * FQDN (Fully Qualified Domain Name) of this home service provider.
     */
@@ -77,6 +80,23 @@ public final class HomeSP implements Parcelable {
                Arrays.equals(roamingConsortiumOIs, that.roamingConsortiumOIs);
    }

    /**
     * Validate HomeSP data.
     *
     * @return true on success or false on failure
     */
    public boolean validate() {
        if (TextUtils.isEmpty(fqdn)) {
            Log.d(TAG, "Missing FQDN");
            return false;
        }
        if (TextUtils.isEmpty(friendlyName)) {
            Log.d(TAG, "Missing friendly name");
            return false;
        }
        return true;
    }

    public static final Creator<HomeSP> CREATOR =
        new Creator<HomeSP>() {
            @Override
+72 −1
Original line number Diff line number Diff line
@@ -16,8 +16,10 @@

package android.net.wifi.hotspot2;

import static org.junit.Assert.assertFalse;
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.os.Parcel;
@@ -44,7 +46,9 @@ public class PasspointConfigurationTest {
        cred.realm = "realm";
        cred.userCredential = null;
        cred.certCredential = null;
        cred.simCredential = null;
        cred.simCredential = new Credential.SimCredential();
        cred.simCredential.imsi = "1234*";
        cred.simCredential.eapType = EAPConstants.EAP_SIM;
        cred.caCertificate = null;
        cred.clientCertificateChain = null;
        cred.clientPrivateKey = null;
@@ -61,11 +65,20 @@ public class PasspointConfigurationTest {
        assertTrue(readConfig.equals(writeConfig));
    }

    /**
     * Verify parcel read/write for a default configuration.
     *
     * @throws Exception
     */
    @Test
    public void verifyParcelWithDefault() throws Exception {
        verifyParcel(new PasspointConfiguration());
    }

    /**
     * Verify parcel read/write for a configuration that contained both HomeSP and Credential.
     * @throws Exception
     */
    @Test
    public void verifyParcelWithHomeSPAndCredential() throws Exception {
        PasspointConfiguration config = new PasspointConfiguration();
@@ -74,6 +87,11 @@ public class PasspointConfigurationTest {
        verifyParcel(config);
    }

    /**
     * Verify parcel read/write for a configuration that contained only HomeSP.
     *
     * @throws Exception
     */
    @Test
    public void verifyParcelWithHomeSPOnly() throws Exception {
        PasspointConfiguration config = new PasspointConfiguration();
@@ -81,10 +99,63 @@ public class PasspointConfigurationTest {
        verifyParcel(config);
    }

    /**
     * Verify parcel read/write for a configuration that contained only Credential.
     *
     * @throws Exception
     */
    @Test
    public void verifyParcelWithCredentialOnly() throws Exception {
        PasspointConfiguration config = new PasspointConfiguration();
        config.credential = createCredential();
        verifyParcel(config);
    }

    /**
     * Verify that a default/empty configuration is invalid.
     *
     * @throws Exception
     */
    @Test
    public void validateDefaultConfig() throws Exception {
        PasspointConfiguration config = new PasspointConfiguration();
        assertFalse(config.validate());
    }

    /**
     * Verify that a configuration without Credential is invalid.
     *
     * @throws Exception
     */
    @Test
    public void validateConfigWithoutCredential() throws Exception {
        PasspointConfiguration config = new PasspointConfiguration();
        config.homeSp = createHomeSp();
        assertFalse(config.validate());
    }

    /**
     * Verify that a a configuration without HomeSP is invalid.
     *
     * @throws Exception
     */
    @Test
    public void validateConfigWithoutHomeSp() throws Exception {
        PasspointConfiguration config = new PasspointConfiguration();
        config.credential = createCredential();
        assertFalse(config.validate());
    }

    /**
     * Verify a valid configuration.
     *
     * @throws Exception
     */
    @Test
    public void validateValidConfig() throws Exception {
        PasspointConfiguration config = new PasspointConfiguration();
        config.homeSp = createHomeSp();
        config.credential = createCredential();
        assertTrue(config.validate());
    }
}
 No newline at end of file
+373 −5

File changed.

Preview size limit exceeded, changes collapsed.

Loading