/**
* Copyright (c) Codice Foundation
* <p/>
* This is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser
* General Public License as published by the Free Software Foundation, either version 3 of the
* License, or any later version.
* <p/>
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details. A copy of the GNU Lesser General Public License
* is distributed along with this program and can be found at
* <http://www.gnu.org/licenses/lgpl.html>.
*/
package org.codice.ddf.admin.insecure.defaults.service;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Path;
import java.security.Key;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.List;
import org.apache.commons.lang.StringUtils;
import org.codice.ddf.admin.insecure.defaults.service.Alert.Level;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class KeystoreValidator implements Validator {
static final String GENERIC_INSECURE_DEFAULTS_MSG = "Unable to determine if keystore [%s] is using insecure defaults. ";
static final String DEFAULT_KEY_PASSWORD_USED_MSG = "The key for alias [%s] in [%s] is using the default password of [%s].";
static final String DEFAULT_KEYSTORE_PASSWORD_USED_MSG = "The keystore password for [%s] is the default password of [%s].";
static final String INVALID_BLACKLIST_KEYSTORE_PASSWORD_MSG = "Unable to determine if keystore [%s] contains insecure default certificates. Error retrieving certificates from Blacklist keystore [%s]. %s.";
static final String BLACKLIST_KEYSTORE_DOES_NOT_EXIST_MSG =
GENERIC_INSECURE_DEFAULTS_MSG + "Cannot read Blacklist keystore [%s].";
static final String CERT_CHAIN_CONTAINS_BLACKLISTED_CERT_MSG = "The certificate chain for alias [%s] in [%s] contains a blacklisted certificate with alias [%s].";
static final String CERT_IS_BLACKLISTED_MSG = "The certificate for alias [%s] in [%s] is a blacklisted certificate with alias [%s].";
static final String KEYSTORE_DOES_NOT_EXIST_MSG =
GENERIC_INSECURE_DEFAULTS_MSG + "Cannot read keystore.";
private static final Logger LOGGER = LoggerFactory.getLogger(KeystoreValidator.class);
List<Alert> alerts;
private String keystorePassword;
private Path keystorePath;
private String defaultKeystorePassword;
private String blacklistKeystorePassword;
private Path blacklistKeystorePath;
private KeyStore blacklistKeystore;
private String defaultKeyPassword;
public KeystoreValidator() {
alerts = new ArrayList<>();
}
public void setKeystorePath(Path path) {
this.keystorePath = path;
}
public void setKeystorePassword(String password) {
this.keystorePassword = password;
}
public void setDefaultKeystorePassword(String password) {
this.defaultKeystorePassword = password;
}
public void setBlacklistKeystorePath(Path path) {
this.blacklistKeystorePath = path;
}
public void setBlacklistKeystorePassword(String password) {
this.blacklistKeystorePassword = password;
}
public void setDefaultKeyPassword(String password) {
this.defaultKeyPassword = password;
}
public List<Alert> validate() {
alerts = new ArrayList<>();
if (isInitialized()) {
List<Certificate> blacklistedCertificates = getBlackListedCertificates();
KeyStore keystore = loadKeystore();
if (keystore != null) {
validateKeyPasswords(keystore);
if (!blacklistedCertificates.isEmpty()) {
List<Certificate[]> keystoreCertificateChains = getKeystoreCertificatesChains(
keystore);
validateKeystoreCertificates(keystore, keystoreCertificateChains,
blacklistedCertificates);
}
}
}
for (Alert alert : alerts) {
LOGGER.debug("Alert: {}, {}", alert.getLevel(), alert.getMessage());
}
return alerts;
}
private boolean isInitialized() {
int errors = 0;
if (keystorePath == null || (keystorePath != null && StringUtils
.isBlank(keystorePath.toString()))) {
alerts.add(new Alert(Level.WARN,
"Unable to determine if keystore is using insecure defaults. No keystore path provided."));
return false;
}
if (blacklistKeystorePath == null || (blacklistKeystorePath != null && StringUtils
.isBlank(blacklistKeystorePath.toString()))) {
alerts.add(new Alert(Level.WARN,
String.format(GENERIC_INSECURE_DEFAULTS_MSG, keystorePath)
+ "No Blacklist keystore path provided."));
return false;
}
if (StringUtils.isBlank(keystorePassword)) {
errors++;
alerts.add(new Alert(Level.WARN,
String.format(GENERIC_INSECURE_DEFAULTS_MSG, keystorePath.toString())
+ "No keystore password provided."));
}
if (StringUtils.isBlank(blacklistKeystorePassword)) {
errors++;
alerts.add(new Alert(Level.WARN,
String.format(GENERIC_INSECURE_DEFAULTS_MSG, keystorePath)
+ "Password for Blacklist keystore [" + blacklistKeystorePath.toString()
+ "] was not provided."));
}
return errors == 0;
}
private KeyStore loadKeystore() {
KeyStore keystore = null;
try {
keystore = KeyStore.getInstance("JKS");
} catch (KeyStoreException e) {
LOGGER.warn(String.format(GENERIC_INSECURE_DEFAULTS_MSG, keystorePath.toString()), e);
alerts.add(new Alert(Level.WARN,
String.format(GENERIC_INSECURE_DEFAULTS_MSG, keystorePath) + e.getMessage()
+ "."));
return null;
}
if (!new File(keystorePath.toString()).canRead()) {
alerts.add(new Alert(Level.WARN,
String.format(KEYSTORE_DOES_NOT_EXIST_MSG, keystorePath)));
return null;
}
try (FileInputStream fis = new FileInputStream(keystorePath.toString())) {
if (StringUtils.isNotBlank(keystorePassword)) {
keystore.load(fis, keystorePassword.toCharArray());
if (StringUtils.equals(keystorePassword, defaultKeystorePassword)) {
alerts.add(new Alert(Level.WARN,
String.format(DEFAULT_KEYSTORE_PASSWORD_USED_MSG,
keystorePath.toString(), defaultKeystorePassword)));
}
}
} catch (NoSuchAlgorithmException | CertificateException | IOException e) {
keystore = null;
LOGGER.warn(String.format(GENERIC_INSECURE_DEFAULTS_MSG, keystorePath), e);
alerts.add(new Alert(Level.WARN,
String.format(GENERIC_INSECURE_DEFAULTS_MSG, keystorePath) + e.getMessage()
+ "."));
}
return keystore;
}
private void validateKeyPasswords(KeyStore keystore) {
try {
Enumeration<String> aliases = keystore.aliases();
while (aliases.hasMoreElements()) {
String alias = (String) aliases.nextElement();
if (keystore.entryInstanceOf(alias, KeyStore.PrivateKeyEntry.class) || keystore
.entryInstanceOf(alias, KeyStore.SecretKeyEntry.class)) {
if (StringUtils.isNotBlank(defaultKeyPassword)) {
// See if we can access the key using the default key password. If we
// cannot, we
// know that we are using a non-default password.
Key key = keystore.getKey(alias, defaultKeyPassword.toCharArray());
if (key != null) {
alerts.add(new Alert(Level.WARN,
String.format(DEFAULT_KEY_PASSWORD_USED_MSG, alias,
keystorePath, defaultKeyPassword)));
}
} else {
alerts.add(new Alert(Level.WARN,
String.format(GENERIC_INSECURE_DEFAULTS_MSG, keystorePath)
+ "No key password provided."));
}
}
}
} catch (UnrecoverableKeyException e) {
// Key is not using default key password.
} catch (KeyStoreException | NoSuchAlgorithmException e) {
LOGGER.warn(String.format(GENERIC_INSECURE_DEFAULTS_MSG, keystorePath), e);
alerts.add(new Alert(Level.WARN,
String.format(GENERIC_INSECURE_DEFAULTS_MSG, keystorePath) + e.getMessage()
+ "."));
}
}
private List<Certificate> getBlackListedCertificates() {
List<Certificate> blacklistedCertificates = new ArrayList<>();
if (!new File(blacklistKeystorePath.toString()).canRead()) {
alerts.add(new Alert(Level.WARN,
String.format(BLACKLIST_KEYSTORE_DOES_NOT_EXIST_MSG, keystorePath,
blacklistKeystorePath)));
return blacklistedCertificates;
}
try (FileInputStream fis = new FileInputStream(blacklistKeystorePath.toString())) {
blacklistKeystore = KeyStore.getInstance("JKS");
blacklistKeystore.load(fis, blacklistKeystorePassword.toCharArray());
Enumeration<String> aliases = blacklistKeystore.aliases();
while (aliases.hasMoreElements()) {
String alias = (String) aliases.nextElement();
Certificate certificate = blacklistKeystore.getCertificate(alias);
blacklistedCertificates.add(certificate);
}
} catch (KeyStoreException | NoSuchAlgorithmException | CertificateException | IOException e) {
String msg = String.format(INVALID_BLACKLIST_KEYSTORE_PASSWORD_MSG, keystorePath,
blacklistKeystorePath, e.getMessage());
LOGGER.warn(msg, e);
alerts.add(new Alert(Level.WARN, msg));
}
return blacklistedCertificates;
}
private List<Certificate[]> getKeystoreCertificatesChains(KeyStore keystore) {
List<Certificate[]> keystoreCertificateChains = new ArrayList<>();
try {
Enumeration<String> aliases = keystore.aliases();
while (aliases.hasMoreElements()) {
String alias = (String) aliases.nextElement();
Certificate[] certificateChain = keystore.getCertificateChain(alias);
if (certificateChain != null) {
keystoreCertificateChains.add(certificateChain);
} else {
Certificate certificate = keystore.getCertificate(alias);
keystoreCertificateChains.add(new Certificate[] {certificate});
}
}
} catch (KeyStoreException e) {
LOGGER.warn(String.format(GENERIC_INSECURE_DEFAULTS_MSG, keystorePath), e);
}
return keystoreCertificateChains;
}
private void validateKeystoreCertificates(KeyStore keystore,
List<Certificate[]> keystoreCertificateChains,
List<Certificate> blackistedCertificates) {
for (Certificate[] certificateChain : keystoreCertificateChains) {
// validate each certificate chain against the blacklist
validateCertificateChain(certificateChain, blackistedCertificates, keystore);
}
}
private void validateCertificateChain(Certificate[] certificateChain,
List<Certificate> blacklistedCertificates, KeyStore keystore) {
Certificate headCertificate = certificateChain[0];
for (Certificate certificate : certificateChain) {
// validate each certificate in the certificate chain against the blacklist
validateAgainstBlacklist(headCertificate, certificate, blacklistedCertificates,
keystore, certificateChain.length);
}
}
private void validateAgainstBlacklist(Certificate headCertificate, Certificate certificate,
List<Certificate> blacklistedCertificates, KeyStore keystore, int certChainLength) {
for (Certificate blackListedCertificate : blacklistedCertificates) {
try {
if (areCertificatesEqual(certificate, blackListedCertificate)) {
String msg = null;
if (certChainLength > 1) {
msg = String.format(CERT_CHAIN_CONTAINS_BLACKLISTED_CERT_MSG,
keystore.getCertificateAlias(headCertificate), keystorePath,
blacklistKeystore.getCertificateAlias(blackListedCertificate));
} else {
msg = String.format(CERT_IS_BLACKLISTED_MSG,
keystore.getCertificateAlias(headCertificate), keystorePath,
blacklistKeystore.getCertificateAlias(blackListedCertificate));
}
alerts.add(new Alert(Level.WARN, msg));
}
} catch (CertificateEncodingException | KeyStoreException e) {
LOGGER.warn(String.format(GENERIC_INSECURE_DEFAULTS_MSG, keystorePath), e);
alerts.add(new Alert(Level.WARN,
String.format(GENERIC_INSECURE_DEFAULTS_MSG, keystorePath) + e
.getMessage()));
}
}
}
private boolean areCertificatesEqual(Certificate certificate,
Certificate blacklistedCertificate) throws CertificateEncodingException {
return Arrays.equals(certificate.getEncoded(), blacklistedCertificate.getEncoded());
}
}