Как заменить статическое хранилище ключей динамическим для приложения spring-boot

Я хочу динамически добавлять/заменять SSL-сертификаты в моем приложении весенней загрузки (tomcat) без необходимости его перезапуска. Мне еще предстоит пройти долгий путь, но в настоящее время я застрял с javax.crypto.BadPaddingException и не знаю, почему.

Итак, вот что я пытаюсь сделать.

Во-первых, я определяю свой собственный TomcatServletWebServerFactory, чтобы установить SslStoreProvider.

@Component
public class PathWatchingTomcatFactory extends TomcatServletWebServerFactory {
  public PathWatchingTomcatFactory(PathWatchingSslStoreProvider pathWatchingSslStoreProvider) {
    setSslStoreProvider(pathWatchingSslStoreProvider);
  }
}

Мой PathWatchingSslStoreProvider обеспечивает PathMatchingKeyStore.

@Component
public class PathWatchingSslStoreProvider implements SslStoreProvider {
  private final PathWatchingKeyStore pathWatchingKeyStore;

  public PathWatchingSslStoreProvider(PathWatchingKeyStore pathWatchingKeyStore) {
    this.pathWatchingKeyStore = pathWatchingKeyStore;
  }

  @Override
  public KeyStore getKeyStore() throws Exception {
    return pathWatchingKeyStore;
  }
}

PathWatchingKeyStore кажется необходимым только для того, чтобы предоставить ему интерфейс поставщика услуг.

@Component
public class PathWatchingKeyStore extends KeyStore {
  protected PathWatchingKeyStore(
    PathWatchingKeyStoreSpi pathWatchingKeyStoreSpi,
    DynamicProvider provider)
  {
    super(pathWatchingKeyStoreSpi, provider, KeyStore.getDefaultType());

    initialize();
  }

  private void initialize() {
    // Loading a keystore marks it internally as initialized and only
    // initialized keystores work properly. Unfortunately
    // nobody initializes this keystore. So we have to do it
    // ourselves.
    //
    // Internally the keystore will delegate loading to the
    // KeyStoreSpi, which, in our case is the PathWatchingKeyStoreSpi.
    try {
      load(null, null);
    }
    catch (Exception e) {
      e.printStackTrace();
    }
  }
}

Теперь при запуске будет загружено хранилище ключей. И поскольку я предоставляю SslStoreProvider, мое хранилище ключей будет загружено SslStoreProviderUrlStreamHandlerFactory путем запроса моего PathWatchingKeyStoreSpi на сохранение хранилища ключей в ByteArrayOutputStream, содержимое которого, наконец, копируется в InputStream, который используется для загрузки внутреннего хранилища ключей.

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

@Component
public class PathWatchingKeyStoreSpi extends KeyStoreSpi {
  private static final Logger LOGGER = LoggerFactory.getLogger(PathWatchingKeyStoreSpi.class);

  private final Path keyStoreLocation;

  public PathWatchingKeyStoreSpi(@Value("${server.ssl.key-store}") Path keyStoreLocation) {
    super();

    this.keyStoreLocation = keyStoreLocation;
  }

  @Override
  public void engineStore(OutputStream stream, char[] password) throws IOException, NoSuchAlgorithmException, CertificateException {
    try {
      final KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
      keyStore.load(new FileInputStream(keyStoreLocation.toString()), "secret".toCharArray());

      // Password must be empty because the SslConnectorCustomizer sets the keystore
      // password used by the tomcat to the empty string if the SslStoreProvider
      // returns a keystore. And because that is what we wanted to do in the first place,
      // providing a dynamic keystore, this is what we have to do.
      keyStore.store(stream, "".toCharArray());
    }
    catch (Exception e) {
      e.printStackTrace();
    }
  }
}

Я вижу, что хранилище ключей загружено, но как только SSLUtilBase пытается прочитать ключ из этого хранилища, возникает исключение BadPaddingException:

Caused by: javax.crypto.BadPaddingException: Given final block not properly padded. Such issues can arise if a bad key is used during decryption.
    at java.base/com.sun.crypto.provider.CipherCore.unpad(CipherCore.java:975) ~[na:na]
    at java.base/com.sun.crypto.provider.CipherCore.fillOutputBuffer(CipherCore.java:1056) ~[na:na]
    at java.base/com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:853) ~[na:na]
    at java.base/com.sun.crypto.provider.PKCS12PBECipherCore.implDoFinal(PKCS12PBECipherCore.java:408) ~[na:na]
    at java.base/com.sun.crypto.provider.PKCS12PBECipherCore$PBEWithSHA1AndDESede.engineDoFinal(PKCS12PBECipherCore.java:440) ~[na:na]
    at java.base/javax.crypto.Cipher.doFinal(Cipher.java:2202) ~[na:na]
    at java.base/sun.security.pkcs12.PKCS12KeyStore.lambda$engineGetKey$0(PKCS12KeyStore.java:406) ~[na:na]
    at java.base/sun.security.pkcs12.PKCS12KeyStore$RetryWithZero.run(PKCS12KeyStore.java:302) ~[na:na]
    at java.base/sun.security.pkcs12.PKCS12KeyStore.engineGetKey(PKCS12KeyStore.java:400) ~[na:na]
    ... 25 common frames omitted

Я создал статическое хранилище ключей, которое я использую здесь, следующим образом:

keytool -genkey -alias tomcat -keyalg RSA

Во-первых, перспективно ли то направление, в котором я собираюсь решать свою проблему? Или я совсем не прав? Сначала я пытался вводить только свои собственные X509ExtendedKeyManager. В отладчике я мог видеть, что сертификат для входящего запроса запрашивается у диспетчера ключей, но, тем не менее, конечная точка tomcat, похоже, инициализируется хранилищем ключей без участия диспетчера.

Кто-нибудь когда-либо пытался внедрить и использовать динамическое хранилище ключей/доверенное хранилище для весеннего загрузочного приложения, используя tomcat в качестве контейнера для серверов?

Любая помощь приветствуется. Тобиас


person Tobias Neubert    schedule 29.09.2019    source источник


Ответы (1)


Хорошо, я не знаю, является ли это окончательным решением, но сейчас оно кажется намного более многообещающим (и менее сложным), чем мой первый способ, описанный выше.

Опять же, все начинается с TomcatServletWebServerFactory. Но на этот раз я установил совершенно новую реализацию JSSEI:

@Component
public class PathWatchingTomcatFactory extends TomcatServletWebServerFactory {
  private final Path keysLocation;

  public PathWatchingTomcatFactory(@Value("${tobias.spring.ssl.keys-location}")Path keysLocation) {
    this.keysLocation = requireNonNull(keysLocation);
  }

  @Override
  protected void customizeConnector(Connector connector) {
    super.customizeConnector(connector);

    connector.setProperty("sslImplementationName", DynamicSslImplementation.class.getName());
    System.setProperty("tobias.spring.ssl.keys-location", keysLocation.toUri().toString());
  }
}

Класс реализации очень прост. Он должен только предоставить собственный экземпляр SSLUtil.

public class DynamicSslImplementation extends JSSEImplementation {
  public DynamicSslImplementation() {
    super();
  }

  @Override
  public SSLUtil getSSLUtil(SSLHostConfigCertificate certificate) {
    return new DynamicSslUtil(certificate);
  }
}

И экземпляр SSLUtil предоставляет мой собственный PathWatchingKeyManager, который будет возвращать ключи из определенного каталога.

public class DynamicSslUtil extends JSSEUtil {
  DynamicSslUtil(SSLHostConfigCertificate certificate) {
    super(certificate);
  }

  @Override
  public KeyManager[] getKeyManagers() {
    return new KeyManager[]{new DynamicKeyManager()};
  }
}

Для свойства server.ssl.key-store должно быть установлено значение NONE.

Кажется, это работает. Приложения весенней загрузки запускаются без сбоев, и DynamicKeyManager запрашивает сертификат для запроса https.

Если это действительно сработает, я опубликую полное решение здесь.

person Tobias Neubert    schedule 01.10.2019
comment
Это работает? Потому что я пытаюсь настроить SslStoreProvider и получаю то же исключение заполнения, что и вы. - person Mr.Robot; 25.01.2021
comment
Я не знаю. Мне жаль. Мы вынесли проблему наружу, используя прокси-сервер перед нашими Java-приложениями, который служит конечной точкой TLS. - person Tobias Neubert; 14.05.2021