Проверка подлинности сертификата клиента без локального хранилища доверенных сертификатов - PullRequest
0 голосов
/ 02 июля 2019

ОК, поначалу это может звучать странно, поэтому, пожалуйста, потерпите меня: -)

Проблема, которую мне нужно решить, такова:
Мне нужно включить аутентификацию клиента в приложении Spring Boot таким образом, чтобы клиент мог сам создать сертификат, без необходимость для сервера подписывать CSR с помощью закрытого ключа сервера.

Как мне достичь этой цели?


Справочная информация: зачем мне это?

Мы настроили сервер Spring Cloud Config. Он содержит значения конфигурации для множества различных приложений. Теперь мы хотим разрешить каждому приложению доступ только к его собственным значениям конфигурации.
Кажется, самое простое, но безопасное решение этой проблемы:

  1. Приложение создает самозаверяющий сертификат
  2. Он хранит сертификат, включая его закрытый ключ, на сервере, на котором он работает, и устанавливает контроль доступа так, чтобы только его пользователь службы имел к нему доступ
  3. Он пытается запросить значения своей конфигурации у сервера Cloud Config.
  4. Не удастся, потому что сертификат клиента неизвестен серверу
  5. Приложение будет регистрировать ошибку с URL-адресом, который оно пыталось получить, и открытым ключом своего сертификата
  6. Пользователь с правами администратора вручную создаст сопоставление между URL-адресом и открытым ключом в безопасном хранилище конфигурации, которое может прочитать Cloud Config Server
  7. Теперь, когда приложение пытается прочитать свои значения конфигурации с сервера, сервер изучит свое безопасное хранилище конфигурации и проверит, является ли оно записью для запрошенного URL-адреса и, если да, подписан ли запрос с частным ключ, который соответствует открытому ключу, который был сохранен для этого URL.
  8. Если все успешно, возвращаются значения конфигурации

Пункт 7 будет реализован как простой Filter.

1 Ответ

1 голос
/ 03 июля 2019

То, чего я хочу достичь, сводится к одной проблеме:
Вместо того, чтобы загружать хранилище доверенных сертификатов из файла, хранилище доверенных сертификатов должно создаваться в памяти на основе данных из безопасного хранилища конфигурации.
ЭтоОказалось, что это немного сложно, но абсолютно возможно.

Создать хранилище доверенных сертификатов очень просто:

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.

.Нам нужно включить нашего провайдера в список провайдеров безопасности.Наш провайдер использует нашу собственную фабрику диспетчера доверия для создания экземпляров нашего собственного диспетчера доверия.

По-прежнему не хватает двух вещей:

  1. Реализация нашего диспетчера доверия
  2. Объяснение для 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");
    }
}

Эта реализация имеет несколько примечательных моментов:

  1. Он фактически делегирует всю работу обычному диспетчеру доверия по умолчанию. Чтобы получить его, нам нужна фабрика диспетчера доверия по умолчанию, которая обычно используется конвейером SSL. Вот почему мы передаем ее в качестве параметра originalTrustManagerFactoryв конструкторе.
  2. На самом деле мы используем два разных экземпляра диспетчера доверия: один для проверки клиентских сертификатов - который используется, когда клиент отправляет нам запрос и аутентифицируется с помощью клиентского сертификата - и другой для проверкисертификаты сервера - которые используются при отправке запроса на сервер с использованием HTTPS. Для проверки сертификата клиентаЕсли мы создаем доверительный менеджер с собственным складом доверенных сертификатов.Он будет содержать только те сертификаты, которые хранятся в нашем безопасном хранилище конфигурации, и, следовательно, он не будет содержать никаких корневых ЦС, которым Java обычно доверяет.Если бы мы использовали этот диспетчер доверия для запросов к URL-адресу HTTPS, где мы являемся клиентом, запрос потерпит неудачу, поскольку мы не сможем проверить действительность сертификата сервера.Поэтому диспетчер доверия для проверки сертификата сервера создается без передачи склада доверенных сертификатов и поэтому будет использовать хранилище доверенных сертификатов Java по умолчанию.
  3. 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 и связанные с ней настройки

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...