/*
|
* 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.net.wifi.WifiConfiguration;
|
import android.net.wifi.WifiEnterpriseConfig;
|
import android.os.Process;
|
import android.security.Credentials;
|
import android.security.KeyChain;
|
import android.security.KeyStore;
|
import android.text.TextUtils;
|
import android.util.ArraySet;
|
import android.util.Log;
|
|
import java.io.ByteArrayInputStream;
|
import java.io.IOException;
|
import java.io.InputStream;
|
import java.security.Key;
|
import java.security.cert.Certificate;
|
import java.security.cert.CertificateException;
|
import java.security.cert.CertificateFactory;
|
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;
|
|
private final KeyStore mKeyStore;
|
|
WifiKeyStore(KeyStore keyStore) {
|
mKeyStore = keyStore;
|
}
|
|
/**
|
* 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);
|
}
|
|
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.
|
* @return true if successful, false otherwise.
|
*/
|
private boolean installKeys(WifiEnterpriseConfig existingConfig, WifiEnterpriseConfig config,
|
String name) {
|
boolean ret = true;
|
String privKeyName = Credentials.USER_PRIVATE_KEY + name;
|
String userCertName = Credentials.USER_CERTIFICATE + name;
|
Certificate[] clientCertificateChain = config.getClientCertificateChain();
|
if (clientCertificateChain != null && clientCertificateChain.length != 0) {
|
byte[] privKeyData = config.getClientPrivateKey().getEncoded();
|
if (mVerboseLoggingEnabled) {
|
if (isHardwareBackedKey(config.getClientPrivateKey())) {
|
Log.d(TAG, "importing keys " + name + " in hardware backed store");
|
} else {
|
Log.d(TAG, "importing keys " + name + " in software backed store");
|
}
|
}
|
ret = mKeyStore.importKey(privKeyName, privKeyData, Process.WIFI_UID,
|
KeyStore.FLAG_NONE);
|
|
if (!ret) {
|
return ret;
|
}
|
|
ret = putCertsInKeyStore(userCertName, clientCertificateChain);
|
if (!ret) {
|
// Remove private key installed
|
mKeyStore.delete(privKeyName, Process.WIFI_UID);
|
return ret;
|
}
|
}
|
|
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 alias = caCertificates.length == 1 ? name
|
: String.format("%s_%d", name, i);
|
|
oldCaCertificatesToRemove.remove(alias);
|
ret = putCertInKeyStore(Credentials.CA_CERTIFICATE + alias, caCertificates[i]);
|
if (!ret) {
|
// Remove client key+cert
|
if (config.getClientCertificate() != null) {
|
mKeyStore.delete(privKeyName, Process.WIFI_UID);
|
mKeyStore.delete(userCertName, Process.WIFI_UID);
|
}
|
// Remove added CA certs.
|
for (String addedAlias : caCertificateAliases) {
|
mKeyStore.delete(Credentials.CA_CERTIFICATE + addedAlias, Process.WIFI_UID);
|
}
|
return ret;
|
} else {
|
caCertificateAliases.add(alias);
|
}
|
}
|
}
|
// Remove old CA certs.
|
for (String oldAlias : oldCaCertificatesToRemove) {
|
mKeyStore.delete(Credentials.CA_CERTIFICATE + oldAlias, Process.WIFI_UID);
|
}
|
// Set alias names
|
if (config.getClientCertificate() != null) {
|
config.setClientCertificateAlias(name);
|
config.resetClientKeyEntry();
|
}
|
|
if (caCertificates != null) {
|
config.setCaCertificateAliases(
|
caCertificateAliases.toArray(new String[caCertificateAliases.size()]));
|
config.resetCaCertificate();
|
}
|
return ret;
|
}
|
|
/**
|
* Install a certificate into the keystore.
|
*
|
* @param name The alias name of the certificate to be installed
|
* @param cert The certificate to be installed
|
* @return true on success
|
*/
|
public boolean putCertInKeyStore(String name, Certificate cert) {
|
return putCertsInKeyStore(name, new Certificate[] {cert});
|
}
|
|
/**
|
* Install a client certificate chain into the keystore.
|
*
|
* @param name The alias name of the certificate to be installed
|
* @param certs The certificate chain to be installed
|
* @return true on success
|
*/
|
public boolean putCertsInKeyStore(String name, Certificate[] certs) {
|
try {
|
byte[] certData = Credentials.convertToPem(certs);
|
if (mVerboseLoggingEnabled) {
|
Log.d(TAG, "putting " + certs.length + " certificate(s) "
|
+ name + " in keystore");
|
}
|
return mKeyStore.put(name, certData, Process.WIFI_UID, KeyStore.FLAG_NONE);
|
} catch (IOException e1) {
|
return false;
|
} catch (CertificateException e2) {
|
return false;
|
}
|
}
|
|
/**
|
* Install a key into the keystore.
|
*
|
* @param name The alias name of the key to be installed
|
* @param key The key to be installed
|
* @return true on success
|
*/
|
public boolean putKeyInKeyStore(String name, Key key) {
|
byte[] privKeyData = key.getEncoded();
|
return mKeyStore.importKey(name, privKeyData, Process.WIFI_UID, KeyStore.FLAG_NONE);
|
}
|
|
/**
|
* Remove a certificate or key entry specified by the alias name from the keystore.
|
*
|
* @param name The alias name of the entry to be removed
|
* @return true on success
|
*/
|
public boolean removeEntryFromKeyStore(String name) {
|
return mKeyStore.delete(name, Process.WIFI_UID);
|
}
|
|
/**
|
* Remove enterprise keys from the network config.
|
*
|
* @param config Config corresponding to the network.
|
*/
|
public void removeKeys(WifiEnterpriseConfig config) {
|
// 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 and user cert");
|
}
|
mKeyStore.delete(Credentials.USER_PRIVATE_KEY + client, Process.WIFI_UID);
|
mKeyStore.delete(Credentials.USER_CERTIFICATE + client, Process.WIFI_UID);
|
}
|
}
|
|
// Do not remove CA certs that were manually installed by the user
|
if (config.isAppInstalledCaCert()) {
|
String[] aliases = config.getCaCertificateAliases();
|
// a valid ca certificate is configured
|
if (aliases != null) {
|
for (String ca : aliases) {
|
if (!TextUtils.isEmpty(ca)) {
|
if (mVerboseLoggingEnabled) {
|
Log.d(TAG, "removing CA cert: " + ca);
|
}
|
mKeyStore.delete(Credentials.CA_CERTIFICATE + ca, Process.WIFI_UID);
|
}
|
}
|
}
|
}
|
}
|
|
|
/**
|
* @param certData byte array of the certificate
|
*/
|
private X509Certificate buildCACertificate(byte[] certData) {
|
try {
|
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
|
InputStream inputStream = new ByteArrayInputStream(certData);
|
X509Certificate caCertificateX509 = (X509Certificate) certificateFactory
|
.generateCertificate(inputStream);
|
return caCertificateX509;
|
} catch (CertificateException e) {
|
return null;
|
}
|
}
|
/**
|
* 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) {
|
WifiEnterpriseConfig enterpriseConfig = config.enterpriseConfig;
|
if (!needsKeyStore(enterpriseConfig)) {
|
return true;
|
}
|
|
try {
|
/* 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);
|
if (!installKeys(existingConfig != null
|
? existingConfig.enterpriseConfig : null, enterpriseConfig, 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 first CA certificate, and initialize
|
byte[] certData = mKeyStore.get(
|
Credentials.CA_CERTIFICATE + config.enterpriseConfig.getCaCertificateAlias(),
|
android.os.Process.WIFI_UID);
|
|
if (certData == null) {
|
Log.e(TAG, "Failed reading CA certificate for Suite-B");
|
return false;
|
}
|
|
X509Certificate x509CaCert = buildCACertificate(certData);
|
|
if (x509CaCert != null) {
|
String sigAlgOid = x509CaCert.getSigAlgOID();
|
if (mVerboseLoggingEnabled) {
|
Log.d(TAG, "Signature algorithm: " + sigAlgOid);
|
}
|
config.allowedSuiteBCiphers.clear();
|
|
// 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
|
config.allowedSuiteBCiphers.set(
|
WifiConfiguration.SuiteBCipher.ECDHE_RSA);
|
if (mVerboseLoggingEnabled) {
|
Log.d(TAG, "Selecting Suite-B RSA");
|
}
|
} else if (sigAlgOid.equals("1.2.840.10045.4.3.3")) {
|
// ecdsa-with-SHA384
|
config.allowedSuiteBCiphers.set(
|
WifiConfiguration.SuiteBCipher.ECDHE_ECDSA);
|
if (mVerboseLoggingEnabled) {
|
Log.d(TAG, "Selecting Suite-B ECDSA");
|
}
|
} else {
|
Log.e(TAG, "Invalid CA certificate type for Suite-B: "
|
+ sigAlgOid);
|
return false;
|
}
|
} else {
|
Log.e(TAG, "Invalid CA certificate for Suite-B");
|
return false;
|
}
|
}
|
return true;
|
}
|
}
|