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