blob: c1706a20d5d8a3034d5d69c176b486a347b89146 [file] [log] [blame]
/*
* 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;
}
}