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