Как создать SSLSocketFactory из сертификата PEM и ключа без преобразования в хранилище ключей?

Мне дали самоподписанный клиентский комплект сертификатов, который должен использоваться для доступа к серверу через HTTPS. Комплект состоит из следующих файлов PEM:

  1. client.crt (сертификат клиента)
  2. client.key (закрытый ключ клиента)
  3. ca.crt (сертификат CA)

Одним из способов решения этой задачи является создание хранилища ключей Java:

  1. Используйте openssl для преобразования клиентского сертификата и ключа в хранилище ключей PKCS12.
  2. Используйте keytool для импорта сертификата CA в хранилище

... а затем используйте код, подобный следующему, для создания экземпляра SSLSocketFactory:

InputStream stream = new ByteArrayInputStream(pksData);         
KeyStore keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(stream, password);

KeyManagerFactory kmf = KeyManagerFactory.getInstance(
    KeyManagerFactory.getDefaultAlgorithm());
kmf.init(keyStore, password.toCharArray());
KeyManager[] keyManagers = kmf.getKeyManagers();

TrustManagerFactory tmfactory = TrustManagerFactory.getInstance(
    TrustManagerFactory.getDefaultAlgorithm());
tmfactory.init(keyStore);
TrustManager[] trustManagers = tmfactory.getTrustManagers();

SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(keyManagers, trustManagers, null);
sslSocketFactory = sslContext.getSocketFactory();

... который позже используется для инициализации библиотеки http.

Таким образом, мы получаем KeyStore, затем инициализируем KeyManager и TrustManagers с его помощью и, наконец, мы создаем экземпляр SSLSocketFactory с ними.

Вопрос заключается в следующем: есть ли способ избежать создания файла хранилища ключей и каким-либо образом создать SSLSocketFactory, начиная с PublicKey и экземпляра Certificate (который, например, можно получить из файлов PEM с помощью PemReader от bouncycastle)?

2 ответа

Решение

Оказалось, что экземпляр KeyStore все еще должен быть собран, но это можно сделать в памяти (начиная с файлов PEM в качестве входных данных), без использования промежуточной сборки файла хранилища ключей с помощью keytool.

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

private static final String TEMPORARY_KEY_PASSWORD = "changeit";

private KeyStore getKeyStore() throws ConfigurationException {
    try {
        Certificate clientCertificate = loadCertificate(certificatePem);
        PrivateKey privateKey = loadPrivateKey(privateKeyPem);
        Certificate caCertificate = loadCertificate(caPem);

        KeyStore keyStore = KeyStore.getInstance("JKS");
        keyStore.load(null, null);
        keyStore.setCertificateEntry("ca-cert", caCertificate);
        keyStore.setCertificateEntry("client-cert", clientCertificate);
        keyStore.setKeyEntry("client-key", privateKey, TEMPORARY_KEY_PASSWORD.toCharArray(), new Certificate[]{clientCertificate});
        return keyStore;
    } catch (GeneralSecurityException | IOException e) {
        throw new ConfigurationException("Cannot build keystore", e);
    }
}

private Certificate loadCertificate(String certificatePem) throws IOException, GeneralSecurityException {
    CertificateFactory certificateFactory = CertificateFactory.getInstance("X509");
    final byte[] content = readPemContent(certificatePem);
    return certificateFactory.generateCertificate(new ByteArrayInputStream(content));
}

private PrivateKey loadPrivateKey(String privateKeyPem) throws IOException, GeneralSecurityException {
    return pemLoadPrivateKeyPkcs1OrPkcs8Encoded(privateKeyPem);
}

private byte[] readPemContent(String pem) throws IOException {
    final byte[] content;
    try (PemReader pemReader = new PemReader(new StringReader(pem))) {
        final PemObject pemObject = pemReader.readPemObject();
        content = pemObject.getContent();
    }
    return content;
}

private static PrivateKey pemLoadPrivateKeyPkcs1OrPkcs8Encoded(
        String privateKeyPem) throws GeneralSecurityException, IOException {
    // PKCS#8 format
    final String PEM_PRIVATE_START = "-----BEGIN PRIVATE KEY-----";
    final String PEM_PRIVATE_END = "-----END PRIVATE KEY-----";

    // PKCS#1 format
    final String PEM_RSA_PRIVATE_START = "-----BEGIN RSA PRIVATE KEY-----";
    final String PEM_RSA_PRIVATE_END = "-----END RSA PRIVATE KEY-----";

    if (privateKeyPem.contains(PEM_PRIVATE_START)) { // PKCS#8 format
        privateKeyPem = privateKeyPem.replace(PEM_PRIVATE_START, "").replace(PEM_PRIVATE_END, "");
        privateKeyPem = privateKeyPem.replaceAll("\\s", "");

        byte[] pkcs8EncodedKey = Base64.getDecoder().decode(privateKeyPem);

        KeyFactory factory = KeyFactory.getInstance("RSA");
        return factory.generatePrivate(new PKCS8EncodedKeySpec(pkcs8EncodedKey));

    } else if (privateKeyPem.contains(PEM_RSA_PRIVATE_START)) {  // PKCS#1 format

        privateKeyPem = privateKeyPem.replace(PEM_RSA_PRIVATE_START, "").replace(PEM_RSA_PRIVATE_END, "");
        privateKeyPem = privateKeyPem.replaceAll("\\s", "");

        DerInputStream derReader = new DerInputStream(Base64.getDecoder().decode(privateKeyPem));

        DerValue[] seq = derReader.getSequence(0);

        if (seq.length < 9) {
            throw new GeneralSecurityException("Could not parse a PKCS1 private key.");
        }

        // skip version seq[0];
        BigInteger modulus = seq[1].getBigInteger();
        BigInteger publicExp = seq[2].getBigInteger();
        BigInteger privateExp = seq[3].getBigInteger();
        BigInteger prime1 = seq[4].getBigInteger();
        BigInteger prime2 = seq[5].getBigInteger();
        BigInteger exp1 = seq[6].getBigInteger();
        BigInteger exp2 = seq[7].getBigInteger();
        BigInteger crtCoef = seq[8].getBigInteger();

        RSAPrivateCrtKeySpec keySpec = new RSAPrivateCrtKeySpec(modulus, publicExp, privateExp, prime1, prime2,
                exp1, exp2, crtCoef);

        KeyFactory factory = KeyFactory.getInstance("RSA");

        return factory.generatePrivate(keySpec);
    }

    throw new GeneralSecurityException("Not supported format of a private key");
}

Идея взята из Программно получить KeyStore из PEM

Когда я столкнулся с подобной проблемой, я оставил комментарий к вашему ответу, а теперь вернулся, чтобы предоставить альтернативу для загрузки файла pem. Я создал библиотеку, чтобы упростить себе и другим, см. Здесь: GitHub - SSLContext Kickstart Надеюсь, вам понравится :)

Файлы pem можно загрузить с помощью следующего фрагмента:

      var keyManager = PemUtils.loadIdentityMaterial("certificate-chain.pem", "private-key.pem");
var trustManager = PemUtils.loadTrustMaterial("some-trusted-certificate.pem");

var sslFactory = SSLFactory.builder()
          .withIdentityMaterial(keyManager)
          .withTrustMaterial(trustManager)
          .build();

var sslContext = sslFactory.getSslContext();
var sslSocketFactory = sslFactory.getSslSocketFactory();

Возвращаясь к вашему основному вопросу, я также обнаружил, что невозможно создать SSLSocketFactory без KeyStores. И хранилище ключей в памяти отлично работает, как вы предложили для этого варианта использования.

Другие вопросы по тегам