Цепочки сертификатов сервера Java, не связанные с полем SNI

#java #openssl #tls1.2 #sni #tls1.3

Вопрос:

Я работаю на Java 11.

 >     bash-4.4$ java -version
>     openjdk version "11.0.12" 2021-07-20
>     OpenJDK Runtime Environment (build 11.0.12 7-suse-3.62.1-x8664)
>     OpenJDK 64-Bit Server VM (build 11.0.12 7-suse-3.62.1-x8664, mixed mode)
 

Мой сценарий таков:

  • Мой клиент TLS использует расширение SNI и устанавливает в поле имени сервера значение CN поля SubjectDN корневого CA.
  • Мой сервер должен использовать SNI для выбора промежуточного сертификата CA для отправки вместе с сертификатом сервера при квитировании TLS.
  • Сертификат сервера перекрестно подписан двумя вложенными сертификатами в соответствии с этим рисунком:
 RootCA1        RootCA2
|              |
 --            |
    |          |
     -->Sub2*   -> Sub2
        |          |
         ---------> -> ServerCertificate
 

Обратите внимание, что:

  • Sub2 и Sub2 * имеют один и тот же SubjectDN, но у них разные IssuerDN.
  • IssuerDN Sub2 — это RootCA2
  • IssuerDN Sub2 * — это RootCA1

В хранилище ключей сервера у меня есть две записи:

  • [KeyStore-Entry-0] {псевдоним: «Первичный», ключ, [Сертификат сервера, Sub2]}
  • [KeyStore-Entry-1] {псевдоним: «Вторичный», ключ, [Сертификат сервера, Sub2 *]}

SNIMatcher, добавленный в параметры SSL, на сервере Java настроен на прием чего-то вроде «RootCA *»

Я использую openssl для тестирования этой настройки таким образом:

 openssl s_client -servername 'RootCA1' -CAfile ./RootCA1.pem -connect localhost:4321
openssl s_client -servername 'RootCA2' -CAfile ./RootCA2.pem -connect localhost:4321
 

ОЖИДАЕМОЕ ПОВЕДЕНИЕ:
Я ожидал, что Java KeyManager адресует запросы SNI в соответствии с полем IssuerDN доверенных сертификатов Sub2s. Но это не работает.

При приветствии клиента с SNI, установленным на «RootCA1», мой сервер должен вернуть цепочку «serverCertificate Sub2 *» При приветствии клиента с SNI, установленным на «RootCA2», мой сервер должен вернуть цепочку «serverCertificate Sub2»

НАБЛЮДАЕМОЕ ПОВЕДЕНИЕ: К сожалению, наблюдаемое поведение заключается в том, что мой сервер всегда возвращает цепочку «serverCertificate Sub2», и следующее рукопожатие завершается неудачно

 openssl s_client -servername 'RootCA1' -CAfile ./RootCA1.pem -connect localhost:4321
...
Verification error: unable to get local issuer certificate
...
 

Я безуспешно пытался поиграть с псевдонимами в моем хранилище ключей или с порядком ключей…

Есть идеи? Что я делаю не так? Как мой сервер может вернуть цепочку «serverCertificate Sub2 *», когда для SNI клиента установлено значение RootCA1?

Thank you!

SOLUTION

I managed to find a solution and want to share it for those who will meet the same problem.

  • The first step was to set the SNI Matcher in the SSL Parameters using a proper regular expression. The effect of the javax.net.ssl.SNIMatcher is just to reject wrong client requests returning a TLS Alert «Unrecognized Name».
  • The second step was to populate the KeyStore using aliases in lower case.
  • The third step was to extend the X509ExtendedKeyManager with my own implementation to override the method chooseServerAlias. The method chooseServerAlias (and similarly chooseEngineServerAlias depending on the implementation) is needed to inspect the SNI sent by openssl client and to return the exact keystore alias used later to get the correct certificate chain.

Here’s the code snapshot.

Note that SniKeyManager is wrapping the ExtendedKeyManager built by KeyManagerFactory, then it is passed to SSLContext.init.
Probably there’s some better solution, but I prefer to keep the original X509ExtendedKeyManager build by KeyManagerFactory that also owns the KeyStore.

Here’s how I set my SniKeyManager into the SSLContext:

         for (KeyManager keyManager : keyManagerFactory.getKeyManagers()) {
            if (keyManager instanceof X509ExtendedKeyManager) {
                x509KeyManager = (X509ExtendedKeyManager) keyManager;
                break;
            }
        }
        SniKeyManager sniKeyManager = new SniKeyManager(x509KeyManager);
        sslContext.init(new KeyManager[] {sniKeyManager}, null, null);
 

Вот снимок SniKeyManager:

 public final class SniKeyManager extends X509ExtendedKeyManager {
    private final X509ExtendedKeyManager keyManager;

    public SniKeyManager(X509ExtendedKeyManager keyManager) {
        this.keyManager = keyManager;
    }

    @Override
    public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
        String result = this.keyManager.chooseServerAlias(keyType, issuers, socket);
        //chooseServerAlias may return null on invalid keyType
        
        if (result != null amp;amp; socket instanceof SSLSocket) {
            SSLSocket s = (SSLSocket) socket;
            ExtendedSSLSession session = (ExtendedSSLSession) s.getHandshakeSession();
            List<SNIServerName> snis = session.getRequestedServerNames();
            for (SNIServerName sniValue : snis) {
                result = getKeyStoreAliasBySni(sniValue);
                if (null != result) break;
            }
        }
        //note that result must be still in lower case
        return resu<
    }
    ...
    @Override
    public X509Certificate[] getCertificateChain(String alias) {
        return keyManager.getCertificateChain(alias);
    }

    @Override
    public PrivateKey getPrivateKey(String alias) {
        return keyManager.getPrivateKey(alias);
    }
}
 

Метод SniKeyManager.getKeyStoreAliasBySni содержит логику для привязки значения SNI к псевдониму хранилища ключей.

Класс Java ServerHandshaker (ссылка) обрабатывает подтверждение связи TLS с помощью X509ExtendedKeyManager. Там вы можете видеть, что значение, возвращаемое chooseServerAlias, будет записью для getPrivateKey и getCertificateChain .

Комментарии:

1. Эта «проблема» возникает, когда вы используете SNI для хранения чего-либо, отличного от доменного имени сервера, поскольку SNI определяется всеми применимыми стандартами как всегда и содержит только доменное имя сервера. Использование чего-либо еще запрещено, и никакое программное обеспечение, разработанное в соответствии со стандартами, не может поддерживать что-либо еще. Хотя это хорошо изучено и (замечательно) понятно, я не могу проголосовать за это как за полезное, потому что это на самом деле вредно..