То, чего я хочу достичь, сводится к одной проблеме:
Вместо того, чтобы загружать хранилище доверенных сертификатов из файла, хранилище доверенных сертификатов должно создаваться в памяти на основе данных из безопасного хранилища конфигурации.
ЭтоОказалось, что это немного сложно, но абсолютно возможно.
Создать хранилище доверенных сертификатов очень просто:
KeyStore ts = KeyStore.getInstance(KeyStore.getDefaultType());
ts.load(null);
for (Certificate cert : certList) {
ts.setCertificateEntry(UUID.randomUUID().toString(), cert);
}
Однако обеспечить его конвейером обработки SSL довольно сложно.По сути, нам нужно предоставить реализацию X509ExtendedTrustManager
, использующую склад доверенных сертификатов, который мы создали выше.
Чтобы сделать эту реализацию известной конвейеру обработки SSL, нам нужно реализовать нашего собственного провайдера:
public class ReloadableTrustManagerProvider extends Provider {
public ReloadableTrustManagerProvider() {
super("ReloadableTrustManager", 1, "Provider to load client certificates from memory");
put("TrustManagerFactory." + TrustManagerFactory.getDefaultAlgorithm(), ReloadableTrustManagerFactory.class.getName());
}
}
Этот провайдер, в свою очередь, использует реализацию TrustManagerFactorySpi
:
public class ReloadableTrustManagerFactory extends TrustManagerFactorySpi {
private final TrustManagerFactory originalTrustManagerFactory;
public ReloadableTrustManagerFactory() throws NoSuchAlgorithmException {
ProviderList originalProviders = ProviderList.newList(
Arrays.stream(Security.getProviders()).filter(p -> p.getClass() != ReloadableTrustManagerProvider.class)
.toArray(Provider[]::new));
Provider.Service service = originalProviders.getService("TrustManagerFactory", TrustManagerFactory.getDefaultAlgorithm());
originalTrustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm(), service.getProvider());
}
@Override
protected void engineInit(KeyStore keyStore) throws KeyStoreException {
}
@Override
protected void engineInit(ManagerFactoryParameters managerFactoryParameters) throws InvalidAlgorithmParameterException {
}
@Override
protected TrustManager[] engineGetTrustManagers() {
try {
return new TrustManager[]{new ReloadableX509TrustManager(originalTrustManagerFactory)};
} catch (Exception e) {
return new TrustManager[0];
}
}
}
Подробнее о originalTrustManagerFactory
и ReloadableX509TrustManager
позже.
Наконец, нам нужно зарегистрировать провайдера вспособ, который делает его по умолчанию, так что конвейер SSL будет использовать его:
Security.insertProviderAt(new ReloadableTrustManagerProvider(), 1);
Этот код может быть выполнен в main
, до SpringApplication.run
.
.Нам нужно включить нашего провайдера в список провайдеров безопасности.Наш провайдер использует нашу собственную фабрику диспетчера доверия для создания экземпляров нашего собственного диспетчера доверия.
По-прежнему не хватает двух вещей:
- Реализация нашего диспетчера доверия
- Объяснение для
originalTrustManagerFactory
Во-первых, реализация (на основе https://donneyfan.com/blog/dynamic-java-truststore-for-a-jax-ws-client):
public class ReloadableX509TrustManager extends X509ExtendedTrustManager implements X509TrustManager {
private final TrustManagerFactory originalTrustManagerFactory;
private X509ExtendedTrustManager clientCertsTrustManager;
private X509ExtendedTrustManager serverCertsTrustManager;
private ArrayList<Certificate> certList;
private static Log logger = LogFactory.getLog(ReloadableX509TrustManager.class);
public ReloadableX509TrustManager(TrustManagerFactory originalTrustManagerFactory) throws Exception {
try {
this.originalTrustManagerFactory = originalTrustManagerFactory;
certList = new ArrayList<>();
/* Example on how to load and add a certificate. Instead of loading it here, it should be loaded externally and added via addCertificates
// Should get from secure configuration store
String cert64 = "base64 encoded certificate";
byte encodedCert[] = Base64.getDecoder().decode(cert64);
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
X509Certificate cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(encodedCert));
certList.add(cert); */
reloadTrustManager();
} catch (Exception e) {
logger.fatal(e);
throw e;
}
}
/**
* Removes a certificate from the pending list. Automatically reloads the TrustManager
*
* @param cert is not null and was already added
* @throws Exception if cannot be reloaded
*/
public void removeCertificate(Certificate cert) throws Exception {
certList.remove(cert);
reloadTrustManager();
}
/**
* Adds a list of certificates to the manager. Automatically reloads the TrustManager
*
* @param certs is not null
* @throws Exception if cannot be reloaded
*/
public void addCertificates(List<Certificate> certs) throws Exception {
certList.addAll(certs);
reloadTrustManager();
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
clientCertsTrustManager.checkClientTrusted(chain, authType);
}
@Override
public void checkClientTrusted(X509Certificate[] x509Certificates, String s, Socket socket) throws CertificateException {
clientCertsTrustManager.checkClientTrusted(x509Certificates, s, socket);
}
@Override
public void checkClientTrusted(X509Certificate[] x509Certificates, String s, SSLEngine sslEngine) throws CertificateException {
clientCertsTrustManager.checkClientTrusted(x509Certificates, s, sslEngine);
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
serverCertsTrustManager.checkServerTrusted(chain, authType);
}
@Override
public void checkServerTrusted(X509Certificate[] x509Certificates, String s, Socket socket) throws CertificateException {
serverCertsTrustManager.checkServerTrusted(x509Certificates, s, socket);
}
@Override
public void checkServerTrusted(X509Certificate[] x509Certificates, String s, SSLEngine sslEngine) throws CertificateException {
serverCertsTrustManager.checkServerTrusted(x509Certificates, s, sslEngine);
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return ArrayUtils.addAll(serverCertsTrustManager.getAcceptedIssuers(), clientCertsTrustManager.getAcceptedIssuers());
}
private void reloadTrustManager() throws Exception {
KeyStore ts = KeyStore.getInstance(KeyStore.getDefaultType());
ts.load(null);
for (Certificate cert : certList) {
ts.setCertificateEntry(UUID.randomUUID().toString(), cert);
}
clientCertsTrustManager = getTrustManager(ts);
serverCertsTrustManager = getTrustManager(null);
}
private X509ExtendedTrustManager getTrustManager(KeyStore ts) throws NoSuchAlgorithmException, KeyStoreException {
originalTrustManagerFactory.init(ts);
TrustManager tms[] = originalTrustManagerFactory.getTrustManagers();
for (int i = 0; i < tms.length; i++) {
if (tms[i] instanceof X509ExtendedTrustManager) {
return (X509ExtendedTrustManager) tms[i];
}
}
throw new NoSuchAlgorithmException("No X509TrustManager in TrustManagerFactory");
}
}
Эта реализация имеет несколько примечательных моментов:
- Он фактически делегирует всю работу обычному диспетчеру доверия по умолчанию. Чтобы получить его, нам нужна фабрика диспетчера доверия по умолчанию, которая обычно используется конвейером SSL. Вот почему мы передаем ее в качестве параметра
originalTrustManagerFactory
в конструкторе. - На самом деле мы используем два разных экземпляра диспетчера доверия: один для проверки клиентских сертификатов - который используется, когда клиент отправляет нам запрос и аутентифицируется с помощью клиентского сертификата - и другой для проверкисертификаты сервера - которые используются при отправке запроса на сервер с использованием HTTPS. Для проверки сертификата клиентаЕсли мы создаем доверительный менеджер с собственным складом доверенных сертификатов.Он будет содержать только те сертификаты, которые хранятся в нашем безопасном хранилище конфигурации, и, следовательно, он не будет содержать никаких корневых ЦС, которым Java обычно доверяет.Если бы мы использовали этот диспетчер доверия для запросов к URL-адресу HTTPS, где мы являемся клиентом, запрос потерпит неудачу, поскольку мы не сможем проверить действительность сертификата сервера.Поэтому диспетчер доверия для проверки сертификата сервера создается без передачи склада доверенных сертификатов и поэтому будет использовать хранилище доверенных сертификатов Java по умолчанию.
getAcceptedIssuers
необходимо вернуть принятых эмитентов от обоих наших менеджеров доверия, поскольку в этом методемы не знаем, происходит ли проверка сертификата для сертификата клиента или сервера.Это имеет небольшой недостаток в том, что наш trustmanager также будет доверять серверам, которые используют наши самозаверяющие клиентские сертификаты для своих HTTPS.
Чтобы все это работало, нам нужно включить аутентификацию клиента ssl:
server.ssl.key-store: classpath:keyStore.p12 # secures our API with SSL. Needed, to enable client certificates handling
server.ssl.key-store-password: very-secure
server.ssl.client-auth: need
Поскольку мы создаем собственный склад доверенных сертификатов, нам не нужна настройка server.ssl.trust-store
и связанные с ней настройки