| /* |
| * Copyright (C) 2016 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.server.wifi; |
| |
| import android.annotation.Nullable; |
| import android.net.wifi.WifiConfiguration; |
| import android.net.wifi.WifiEnterpriseConfig; |
| import android.security.KeyChain; |
| import android.text.TextUtils; |
| import android.util.ArraySet; |
| import android.util.Log; |
| |
| import com.android.internal.util.Preconditions; |
| import com.android.server.wifi.util.ArrayUtils; |
| |
| import java.security.Key; |
| import java.security.KeyStore; |
| import java.security.KeyStoreException; |
| import java.security.Principal; |
| import java.security.cert.Certificate; |
| import java.security.cert.X509Certificate; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.Set; |
| |
| /** |
| * This class provides the methods to access keystore for certificate management. |
| * |
| * NOTE: This class should only be used from WifiConfigManager! |
| */ |
| public class WifiKeyStore { |
| private static final String TAG = "WifiKeyStore"; |
| |
| private boolean mVerboseLoggingEnabled = false; |
| |
| @Nullable private final KeyStore mKeyStore; |
| |
| WifiKeyStore(@Nullable KeyStore keyStore) { |
| mKeyStore = keyStore; |
| if (mKeyStore == null) { |
| Log.e(TAG, "Unable to retrieve keystore, all key operations will fail"); |
| } |
| } |
| |
| /** |
| * Enable verbose logging. |
| */ |
| void enableVerboseLogging(boolean verbose) { |
| mVerboseLoggingEnabled = verbose; |
| } |
| |
| // Certificate and private key management for EnterpriseConfig |
| private static boolean needsKeyStore(WifiEnterpriseConfig config) { |
| return (config.getClientCertificate() != null || config.getCaCertificate() != null |
| || config.getCaCertificateAlias() != null |
| || config.getClientCertificateAlias() != null); |
| } |
| |
| private static boolean isHardwareBackedKey(Key key) { |
| return KeyChain.isBoundKeyAlgorithm(key.getAlgorithm()); |
| } |
| |
| private static boolean hasHardwareBackedKey(Certificate certificate) { |
| return isHardwareBackedKey(certificate.getPublicKey()); |
| } |
| |
| /** |
| * Install keys for given enterprise network. |
| * |
| * @param existingConfig Existing config corresponding to the network already stored in our |
| * database. This maybe null if it's a new network. |
| * @param config Config corresponding to the network. |
| * @param existingAlias Alias for all the existing key store data stored. |
| * @param alias Alias for all the key store data to store. |
| * @return true if successful, false otherwise. |
| */ |
| private boolean installKeys(WifiEnterpriseConfig existingConfig, WifiEnterpriseConfig config, |
| String existingAlias, String alias) { |
| Preconditions.checkNotNull(mKeyStore); |
| Certificate[] clientCertificateChain = config.getClientCertificateChain(); |
| if (!ArrayUtils.isEmpty(clientCertificateChain)) { |
| if (!putUserPrivKeyAndCertsInKeyStore(alias, config.getClientPrivateKey(), |
| clientCertificateChain)) { |
| return false; |
| } |
| } |
| X509Certificate[] caCertificates = config.getCaCertificates(); |
| Set<String> oldCaCertificatesToRemove = new ArraySet<>(); |
| if (existingConfig != null && existingConfig.getCaCertificateAliases() != null) { |
| oldCaCertificatesToRemove.addAll( |
| Arrays.asList(existingConfig.getCaCertificateAliases())); |
| } |
| List<String> caCertificateAliases = null; |
| if (caCertificates != null) { |
| caCertificateAliases = new ArrayList<>(); |
| for (int i = 0; i < caCertificates.length; i++) { |
| String caAlias = String.format("%s_%d", alias, i); |
| |
| oldCaCertificatesToRemove.remove(caAlias); |
| if (!putCaCertInKeyStore(caAlias, caCertificates[i])) { |
| // cleanup everything on failure. |
| removeEntryFromKeyStore(alias); |
| for (String addedAlias : caCertificateAliases) { |
| removeEntryFromKeyStore(addedAlias); |
| } |
| return false; |
| } |
| caCertificateAliases.add(caAlias); |
| } |
| } |
| // If alias changed, remove the old one. |
| if (!alias.equals(existingAlias)) { |
| // Remove old private keys. |
| removeEntryFromKeyStore(existingAlias); |
| } |
| // Remove any old CA certs. |
| for (String oldAlias : oldCaCertificatesToRemove) { |
| removeEntryFromKeyStore(oldAlias); |
| } |
| // Set alias names |
| if (config.getClientCertificate() != null) { |
| config.setClientCertificateAlias(alias); |
| config.resetClientKeyEntry(); |
| } |
| |
| if (caCertificates != null) { |
| config.setCaCertificateAliases( |
| caCertificateAliases.toArray(new String[caCertificateAliases.size()])); |
| config.resetCaCertificate(); |
| } |
| return true; |
| } |
| |
| /** |
| * Install a CA certificate into the keystore. |
| * |
| * @param alias The alias name of the CA certificate to be installed |
| * @param cert The CA certificate to be installed |
| * @return true on success |
| */ |
| public boolean putCaCertInKeyStore(String alias, Certificate cert) { |
| try { |
| mKeyStore.setCertificateEntry(alias, cert); |
| return true; |
| } catch (KeyStoreException e) { |
| Log.e(TAG, "Failed to put CA certificate in keystore: " + e.getMessage()); |
| return false; |
| } |
| } |
| |
| /** |
| * Install a private key + user certificate into the keystore. |
| * |
| * @param alias The alias name of the key to be installed |
| * @param key The private key to be installed |
| * @param certs User Certificate chain. |
| * @return true on success |
| */ |
| public boolean putUserPrivKeyAndCertsInKeyStore(String alias, Key key, Certificate[] certs) { |
| try { |
| mKeyStore.setKeyEntry(alias, key, null, certs); |
| return true; |
| } catch (KeyStoreException e) { |
| Log.e(TAG, "Failed to put private key or certificate in keystore: " + e.getMessage()); |
| return false; |
| } |
| } |
| |
| /** |
| * Remove a certificate or key entry specified by the alias name from the keystore. |
| * |
| * @param alias The alias name of the entry to be removed |
| * @return true on success |
| */ |
| public boolean removeEntryFromKeyStore(String alias) { |
| Preconditions.checkNotNull(mKeyStore); |
| try { |
| mKeyStore.deleteEntry(alias); |
| return true; |
| } catch (KeyStoreException e) { |
| return false; |
| } |
| } |
| |
| /** |
| * Remove enterprise keys from the network config. |
| * |
| * @param config Config corresponding to the network. |
| */ |
| public void removeKeys(WifiEnterpriseConfig config) { |
| Preconditions.checkNotNull(mKeyStore); |
| // Do not remove keys that were manually installed by the user |
| if (config.isAppInstalledDeviceKeyAndCert()) { |
| String client = config.getClientCertificateAlias(); |
| // a valid client certificate is configured |
| if (!TextUtils.isEmpty(client)) { |
| if (mVerboseLoggingEnabled) { |
| Log.d(TAG, "removing client private key, user cert and CA cert)"); |
| } |
| // if there is only a single CA certificate, then that is also stored with |
| // the same alias, hence will be removed here. |
| removeEntryFromKeyStore(client); |
| } |
| } |
| |
| // Do not remove CA certs that were manually installed by the user |
| if (config.isAppInstalledCaCert()) { |
| String[] aliases = config.getCaCertificateAliases(); |
| if (aliases == null || aliases.length == 0) { |
| return; |
| } |
| // Remove all CA certificate. |
| for (String ca : aliases) { |
| if (!TextUtils.isEmpty(ca)) { |
| if (mVerboseLoggingEnabled) { |
| Log.d(TAG, "removing CA cert: " + ca); |
| } |
| removeEntryFromKeyStore(ca); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Update/Install keys for given enterprise network. |
| * |
| * @param config Config corresponding to the network. |
| * @param existingConfig Existing config corresponding to the network already stored in our |
| * database. This maybe null if it's a new network. |
| * @return true if successful, false otherwise. |
| */ |
| public boolean updateNetworkKeys(WifiConfiguration config, WifiConfiguration existingConfig) { |
| Preconditions.checkNotNull(mKeyStore); |
| Preconditions.checkNotNull(config.enterpriseConfig); |
| WifiEnterpriseConfig enterpriseConfig = config.enterpriseConfig; |
| /* config passed may include only fields being updated. |
| * In order to generate the key id, fetch uninitialized |
| * fields from the currently tracked configuration |
| */ |
| String keyId = config.getKeyIdForCredentials(existingConfig); |
| WifiEnterpriseConfig existingEnterpriseConfig = null; |
| String existingKeyId = null; |
| if (existingConfig != null) { |
| Preconditions.checkNotNull(existingConfig.enterpriseConfig); |
| existingEnterpriseConfig = existingConfig.enterpriseConfig; |
| existingKeyId = existingConfig.getKeyIdForCredentials(existingConfig); |
| } |
| if (!needsKeyStore(enterpriseConfig)) { |
| return true; |
| } |
| |
| try { |
| if (!installKeys(existingEnterpriseConfig, enterpriseConfig, existingKeyId, keyId)) { |
| Log.e(TAG, config.SSID + ": failed to install keys"); |
| return false; |
| } |
| } catch (IllegalStateException e) { |
| Log.e(TAG, config.SSID + " invalid config for key installation: " + e.getMessage()); |
| return false; |
| } |
| |
| // For WPA3-Enterprise 192-bit networks, set the SuiteBCipher field based on the |
| // CA certificate type. Suite-B requires SHA384, reject other certs. |
| if (config.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.SUITE_B_192)) { |
| // Read the CA certificates, and initialize |
| String[] caAliases = config.enterpriseConfig.getCaCertificateAliases(); |
| |
| if (caAliases == null || caAliases.length == 0) { |
| Log.e(TAG, "No CA aliases in profile"); |
| return false; |
| } |
| |
| int caCertType = -1; |
| int prevCaCertType = -1; |
| for (String caAlias : caAliases) { |
| Certificate caCert = null; |
| try { |
| caCert = mKeyStore.getCertificate(caAlias); |
| } catch (KeyStoreException e) { |
| Log.e(TAG, "Failed to get Suite-B certificate", e); |
| } |
| if (caCert == null || !(caCert instanceof X509Certificate)) { |
| Log.e(TAG, "Failed reading CA certificate for Suite-B"); |
| return false; |
| } |
| |
| // Confirm that the CA certificate is compatible with Suite-B requirements |
| caCertType = getSuiteBCipherFromCert((X509Certificate) caCert); |
| if (caCertType < 0) { |
| return false; |
| } |
| if (prevCaCertType != -1) { |
| if (prevCaCertType != caCertType) { |
| Log.e(TAG, "Incompatible CA certificates"); |
| return false; |
| } |
| } |
| prevCaCertType = caCertType; |
| } |
| |
| Certificate clientCert = null; |
| try { |
| clientCert = mKeyStore.getCertificate(config.enterpriseConfig |
| .getClientCertificateAlias()); |
| } catch (KeyStoreException e) { |
| Log.e(TAG, "Failed to get Suite-B client certificate", e); |
| } |
| if (clientCert == null || !(clientCert instanceof X509Certificate)) { |
| Log.e(TAG, "Failed reading client certificate for Suite-B"); |
| return false; |
| } |
| |
| int clientCertType = getSuiteBCipherFromCert((X509Certificate) clientCert); |
| if (clientCertType < 0) { |
| return false; |
| } |
| |
| if (clientCertType == caCertType) { |
| config.allowedSuiteBCiphers.clear(); |
| config.allowedSuiteBCiphers.set(clientCertType); |
| } else { |
| Log.e(TAG, "Client certificate for Suite-B is incompatible with the CA " |
| + "certificate"); |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| /** |
| * Get the Suite-B cipher from the certificate |
| * |
| * @param x509Certificate Certificate to process |
| * @return WifiConfiguration.SuiteBCipher.ECDHE_RSA if the certificate OID matches the Suite-B |
| * requirements for RSA certificates, WifiConfiguration.SuiteBCipher.ECDHE_ECDSA if the |
| * certificate OID matches the Suite-B requirements for ECDSA certificates, or -1 otherwise. |
| */ |
| private int getSuiteBCipherFromCert(X509Certificate x509Certificate) { |
| String sigAlgOid = x509Certificate.getSigAlgOID(); |
| if (mVerboseLoggingEnabled) { |
| Principal p = x509Certificate.getSubjectX500Principal(); |
| if (p != null && !TextUtils.isEmpty(p.getName())) { |
| Log.d(TAG, "Checking cert " + p.getName()); |
| } |
| } |
| |
| // Wi-Fi alliance requires the use of both ECDSA secp384r1 and RSA 3072 certificates |
| // in WPA3-Enterprise 192-bit security networks, which are also known as Suite-B-192 |
| // networks, even though NSA Suite-B-192 mandates ECDSA only. The use of the term |
| // Suite-B was already coined in the IEEE 802.11-2016 specification for |
| // AKM 00-0F-AC but the test plan for WPA3-Enterprise 192-bit for APs mandates |
| // support for both RSA and ECDSA, and for STAs it mandates ECDSA and optionally |
| // RSA. In order to be compatible with all WPA3-Enterprise 192-bit deployments, |
| // we are supporting both types here. |
| if (sigAlgOid.equals("1.2.840.113549.1.1.12")) { |
| // sha384WithRSAEncryption |
| if (mVerboseLoggingEnabled) { |
| Log.d(TAG, "Found Suite-B RSA certificate"); |
| } |
| return WifiConfiguration.SuiteBCipher.ECDHE_RSA; |
| } else if (sigAlgOid.equals("1.2.840.10045.4.3.3")) { |
| // ecdsa-with-SHA384 |
| if (mVerboseLoggingEnabled) { |
| Log.d(TAG, "Found Suite-B ECDSA certificate"); |
| } |
| return WifiConfiguration.SuiteBCipher.ECDHE_ECDSA; |
| } |
| Log.e(TAG, "Invalid certificate type for Suite-B: " + sigAlgOid); |
| return -1; |
| } |
| } |